题面描述:
题目描述
给定一个长度为N的数列Ai。
你可以对数列进行若干次操作,每次操作可以从数列中任选一个数,把它移动到数列的开头或者结尾。
求最少经过多少次操作,可以把数列变成单调不减的。“单调不减”意味着数列中的任意一个数都不大于排在它后边的数。
输入格式
第一行是一个正整数N。
第二行是N个正整数Ai。
输出格式
输出一个整数,表示最少需要的操作次数。
样例输入
5
6 3 7 8 6
样例输出
2
数据范围与约定
对于30%的数据,满足1≤n≤10。
对于60% 的数据,满足1≤n≤1000。
对于100% 的数据,满足1≤n≤1000000,1≤Ai≤1000000。
这道题相当玄妙(鉴于数据的水)
当我听到有人这道题直接交了最小上升子序列就拿了50分
这让我悲伤我想了好久的如何处理一个数出现多次。。
本来我也觉得最小上升子序列没有问题,但是发现样例多次没有过之后就。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
但是,中心思想永远是,维护一个序列,除了这个序列的数以外的所有的数都要移动,我先是觉得是子序列,子串,然后都轻易的找出了反例。
最长上升子序列:只要一个数出现了两次肯定错的。
最长上升子串:1 3 4 2 5 最小上升子串是3 4 或者2 5,但只要移动两次,。
然后我聪明的同桌指出了我的最大来连续子串 的错误,这道题,应该是找一个最长连续子序列。
连续即离散化后的序号是连续的。
(离散化emmm,我举个例子,1 8 9 100,可以离散化为 1 2 3 4)
比如说上一个 1 3 4 2 5,最小连续子序列是多少呢?
3 4 5
所以答案是2
但是这道题很恶心的地方在于,某个数可以出现多次,所以离散化有点特殊。
for (int i=1; i<=1000000; i++)
{
if (c[i]>0)
{
e[i]=x;
d[i]=++t;
x=t;
t+=c[i]-1;
}
}
数组解释:
c表示桶排序每一个数字出现的次数,以便离散化以及后面统计重复数字的出现
d表示每一个数在离散化数组中的序号
e表示a[i]前一个数值的离散化序号。 譬如 8 8 10,e[10]=8;
譬如说当前这个数是a[i],
的区别是什么呢?
比如说
7 8 8 10
——
8
——
9
这个离散化结合方程:
for (int i=1; i<=n; i++)
{
f[d[a[i]]][0]++;
f[d[a[i]]][1]=max(f[d[a[i]]][1]+1,max(f[e[a[i]]][0]+1,f[d[a[i]-1]][1]+1));
if (f[d[a[i]]][0]==c[a[i]]) f[d[a[i]]][0]=f[d[a[i]]][1];
f[d[a[i]]][2]=max(f[d[a[i]]][1],f[d[a[i]]][2]+1);
ans=max(ans,f[d[a[i]]][2]);
}
表示离散化后序号为i的数处于j状态时的最长序列长度,就是说作为开头也可以作为一个前缀,不仅可以从当前这个数开始,也可以从之前作为某个序列的结尾开始。
j可以0(作为开头),1(作为中间继承的过程),2(作为结尾)
因为无论如何,哪怕a[i]是否重复出现,作为开头都可以从前面出现过的a[i]或者0开始
作为过程,如果是已经重复出现过,那么可以延长,也可以从上一个数字继承过来,
可以直接在过程中结尾,也可以作为本身上一个结尾的数字下来结尾(或者也可以说直接结尾。。。)。
这样子只有80.
加了那句我没有解释的话就在洛谷上过了。。。。(根据某个数据做的特判)
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000010;
int a[maxn],e[maxn],c[maxn],d[maxn],f[maxn][4];
inline int read()
{
int X=0,w=0; char c=0;
while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
return w?-X:X;
}
int main()
{
//freopen("change.in","r",stdin);
//freopen("change.out","w",stdout);
int n;
n=read();
for (int i=1; i<=n; i++)
{
a[i]=read();
c[a[i]]++;
}
int x=0,t=0;
for (int i=1; i<=1000000; i++)
{
if (c[i]>0)
{
e[i]=x;
d[i]=++t;
x=t;
t+=c[i]-1;
}
}
int ans=0;;
for (int i=1; i<=n; i++)
{
f[d[a[i]]][0]++;
f[d[a[i]]][1]=max(f[d[a[i]]][1]+1,max(f[e[a[i]]][0]+1,f[d[a[i]-1]][1]+1));
if (f[d[a[i]]][0]==c[a[i]]) f[d[a[i]]][0]=f[d[a[i]]][1];
f[d[a[i]]][2]=max(f[d[a[i]]][1],f[d[a[i]]][2]+1);
ans=max(ans,f[d[a[i]]][2]);
}
cout<<n-ans;
}
这个程序是错的。。。但是在洛谷上过的。。。。。
为什么错呢
数据一:
9
5 4 2 1 8 6 7 8 10
ans:5
10
2 2 1 1 3 3 4 4 5 5
ans:2
。。这些数据过不了但是洛谷ac了。。。
等我一下我过会儿写正解。。
正确解法1
…………为什么是错的呢,是因为我们没有办法处理重复的数字的每次承载的。也就是说,我们每次去维护的作为重复出现的数字的开头,只会是这个数字出现的次数,所以,我们去设置一个状态j=3,去记载以该数字结尾的最大前缀,每次都要更新,如果是当前这个状态值是3,那么可以从以这个状态为结尾的上次出现数值为更新,
直到这个数字出现的最后一次,去更新以这个数字为结尾的最大前缀。
而我们需要的以这个数字为开头的状态,即最大前缀,而非这个数字单纯的出现次数。
后来我又发现了
包含了所有
,根本不需要
。
#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],c[1000005],d[1000005],e[1000005],f[1000005][4],t,ans,maxx;//a记录原数组,c是桶,d[i]是值为i的数的编号,e[i]是值为i的数的前一个数的编号
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
c[a[i]]++;
maxx=max(a[i],maxx);
}
int x=0;
for (int i=1;i<=1000000;i++)
{
if (c[i]>0)
{
e[i]=x;
d[i]=++t;
x=t;
t+=c[i]-1;
}
}
for (int i=1;i<=n;i++)
{
c[a[i]]--;
f[d[a[i]]][0]=f[d[a[i]]][0]+1;
f[d[a[i]]][1]=max(f[d[a[i]]][1]+1,f[e[a[i]]][0]+1);
f[d[a[i]]][2]=max(f[d[a[i]]][1],f[d[a[i]]][2]+1);
if (f[d[a[i]]][3]==0) f[d[a[i]]][3]=f[d[a[i]]][2];
else f[d[a[i]]][3]++;
if (c[a[i]]==0) f[d[a[i]]][0]=f[d[a[i]]][3];
ans=max(ans,f[d[a[i]]][2]);
}
cout<<n-ans;
}
正确解法2:
在这道题里,我们可以将该序列中的任何一个数放到后面或前面,而我们要这个操作次数最少,那么我们就要使保持不动的点最多,而如果任意两个点(数)不动,那么就不能将任何原本不在该两个数中间的数插入这两个数中间。
因此,我们就要求出一个连续的最长子序列。
何为连续?
就是说该序列必须是最后的结果序列的子串
即,如果该数的前一个不同的数(pre)和后一个不同的数(next)都在之中,那么这个数必须全部都在pre和next中间。
所以我们可以一个数一个数考虑,并将可以存在的数放在单调队列中
条件1:该队列中的数在保证不减的同时保证原序号递增
条件2:同时如果该序列中将要放入b这个数,而a小于b,若大小等于a的数并没有全部在队列里,那么小于a的数都不能在队列里
#include<bits/stdc++.h>
using namespace std;
int n,m,a[1000010],ans;
deque<int> q;//双端队列模拟单调队列
vector<int> b[1000010];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[a[i]].push_back(i);//将同一个大小的数的id记录下来
//(因为从前向后读入所以保持单调)
m=max(m,a[i]);//最大的数是多少
}
for(int i=1;i<=m;i++){//从小到大将数加入单调队列
int lt=b[i].size();
for(int j=lt-1;j>=0;j--){
int k=b[i][j];//拿出大小为i,id在i中大小为第j大及以后的数中最靠前的数的id
//只要该数能放入队列中,序号比该数大,大小与它相同的都能放入
while(!q.empty()&&q.back()>k){//把队列处理成能将k放入的状态
while(q.size()>=1&&a[q.front()]<a[q.back()]) q.pop_front();
//这个while保证条件二
q.pop_back();
//因为条件一,所以序号越大的数越在队列的后面
}
int lm=q.size();
ans=max(ans,lm+lt-j);//更新答案
}
for(int j=0;j<lt;j++) q.push_back(b[i][j]);//将该数加入队列
}
printf("%d",n-ans);//答案是总长减去最长序列的长度
}