序列变换(O(n))

题面描述:

题目描述
给定一个长度为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], e [ i ] , d [ a [ i ] 1 ] 的区别是什么呢?
比如说
7 8 8 10
e [ 10 ] —— > 8
d [ a [ i ] 1 ] —— > 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]);
      }

f [ i ] [ j ] 表示离散化后序号为i的数处于j状态时的最长序列长度,就是说作为开头也可以作为一个前缀,不仅可以从当前这个数开始,也可以从之前作为某个序列的结尾开始。

j可以0(作为开头),1(作为中间继承的过程),2(作为结尾)

f [ d [ a [ i ] ] ] [ 0 ] 因为无论如何,哪怕a[i]是否重复出现,作为开头都可以从前面出现过的a[i]或者0开始

f [ d [ a [ i ] ] ] [ 1 ] 作为过程,如果是已经重复出现过,那么可以延长,也可以从上一个数字继承过来,

f [ d [ a [ i ] ] ] [ 2 ] 可以直接在过程中结尾,也可以作为本身上一个结尾的数字下来结尾(或者也可以说直接结尾。。。)。

这样子只有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,那么可以从以这个状态为结尾的上次出现数值为更新,
直到这个数字出现的最后一次,去更新以这个数字为结尾的最大前缀。
而我们需要的以这个数字为开头的状态,即最大前缀,而非这个数字单纯的出现次数。
后来我又发现了 e [ a [ i ] ] 包含了所有 d [ a [ i ] 1 ] ,根本不需要 d [ a [ i ] 1 ]

#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的数都不能在队列里

rym的单调队列解法

#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);//答案是总长减去最长序列的长度
}

猜你喜欢

转载自blog.csdn.net/beautiful_CXW/article/details/81483948