前言
这道题非常妙,是我和我同桌花了将近一上午的时间想出来的,想出来之后还发现洛谷题解中没有我们的做法(虽然一共没几篇)。这次因为有些情况没有考虑完全,所以我初测只有70(70分程序还是因为开数组的时候手抖了,开小了,不然能在洛谷上AC)。但把我的洛谷AC程序放到一个数据很强的评测网站上却WA了一半。我又花了一个下午加半个晚上才终于想出了完美的正解,顺利地在数据很强的网站上AC了。
我的做法不同于网上大多数的单调队列做法,他们是O(n*logn)的,而我的做法是O(n)的。这种做法可能思维含量比较高(也许是我语言表达能力太差了),如果实在看不懂就算了。
这是我同桌的博客,可以与我的博客互为注释:
https://blog.csdn.net/beautiful_CXW/article/details/81483948
题目大意
给你n个数,每次可以把任意一个数移到队尾或对头,问至少移几次,可以使序列有序。n<=1000000,每个数小于1000000。
题目分析
怎样移会使次数最少?移动次数最少,就是让最多的数不动。那这些不动的数要满足什么条件?这些数必须是连续的。如果不连续,就要把一些数放到这些数中间,那又做不到,所以这些数必须是连续的。(所谓连续,就是排完序后的位置连续)
题解
若把这些数离散成1~n的互不重复的编号,则只要求一个最长编号连续上升子序列。可这样做要求每个数互不重复,那肯定有些数会重复,这就导致我的程序非常的恶心。
先给出洛谷AC但错误的程序:
#include<iostream>
#include<cstdio>
using namespace std;
int n,a[1000005],c[1000005],d[1000005],e[1000005],f[1000005][3],t,ans;//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]]++;
}
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++)
{
f[d[a[i]]][0]=f[d[a[i]]][0]+1;
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));
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;
}
我的离散化是把相同的数离散成一样的数,而且有了两个一样的数,下一个数的编号与之相差2。比如100 5 5 3 7 6 6 6 8会离散成9 2 2 1 7 4 4 4 8。
那么对于编号相同的数,有以下四种做法:
1.只选一个。
2.全选。
3.选任意个放在开头。
4.选任意个放在末尾。
根据以上四种做法,我们就有了题目的动态规划方程(我也不知道叫不叫动态规划)。
f[i][0]表示编号为i的数作为开头的最长编号连续上升子序列长度(作为开头,就是前面的数编号要么和自己相同,要么没有)。f[i][1]表示编号为i的数放在中间的最长编号连续上升子序列长度。f[i][2]表示编号为i的数作为结尾的最长编号连续上升子序列长度(就是这个数后面不放别的数了)。
对上面的三行与f数组有关的语句进行解释:
f[d[a[i]]][0]=f[d[a[i]]][0]+1;//a[i]是这个数,d[a[i]]就是这个数离散化后的编号。以d[a[i]]作为开头,前面不能有别的数,只能有与自己编号相同的数。
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));//要么前面是与自己编号相同的数;要么前面那个数是编号在自己前面的,并且作为开头的(因为这样不用管那个数选了几个);要么是前面那个编号在自己前面的数只有一个。
f[d[a[i]]][2]=max(f[d[a[i]]][1],f[d[a[i]]][2]+1);//可以立刻结尾结掉,也可以从上一个以与自己编号相同的数作为结尾的状态继承过来。
看出来哪里错了吗?
就是对于编号相同的数全选没有处理好(上面关于f数组第二句话有问题)。
给一组hack数据(方便起见,给出的离散化后的数据):
输入:
8
3 3 1 1 5 5 7 7
1
2
输出:
2
1
以下是完全正确的解法:
#include<iostream>
#include<cstdio>
using namespace std;
int n,a[1000005],c[1000005],d[1000005],e[1000005],f[1000005][4],t,ans,ma;//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]]++;
ma=max(ma,a[i]);
}
int x=0;
for (int i=1;i<=ma;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;
}
可以看出来,程序里新添了一个f[d[a[i]]][3]。它表示,对于每一个出现过的d[a[i]]都必须选的最长编号连续上升子序列长度。当以d[a[i]]为编号的数第一次出现时,它就是以d[a[i]]为结尾的最长编号连续上升子序列。不然每一次,它都把自己放在队尾。等到d[a[i]]全部出现过了之后,就可以更新f[d[a[i]]][0],这时的f[d[a[i]]][0]和原先的定义就不同了。在d[a[i]]全部出现前,定义不变;在d[a[i]]全部出现后,它只能变成全选d[a[i]],且结尾为d[a[i]]的最长编号连续上升子序列长度。因为如果d[a[i]]没有全选,这个数后面就不能接别的数了,这时就没有了意义。
总结
这题本质就是离散化之后求最长编号连续上升子序列长度,不知道有没有别的做法,反正我这个做法是我硬想了一天才想出来的,十分不容易。也许这不一定是最好的方法,但是我认为对我来说最有意义的方法。我也是思考了很久,反复的修改,可能有些步骤略显冗杂,希望大家批评指正。