序列变换O(n)做法

洛谷p3411

前言

这道题非常妙,是我和我同桌花了将近一上午的时间想出来的,想出来之后还发现洛谷题解中没有我们的做法(虽然一共没几篇)。这次因为有些情况没有考虑完全,所以我初测只有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]]没有全选,这个数后面就不能接别的数了,这时就没有了意义。

总结

这题本质就是离散化之后求最长编号连续上升子序列长度,不知道有没有别的做法,反正我这个做法是我硬想了一天才想出来的,十分不容易。也许这不一定是最好的方法,但是我认为对我来说最有意义的方法。我也是思考了很久,反复的修改,可能有些步骤略显冗杂,希望大家批评指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值