序列變換(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);//答案是總長減去最長序列的長度
}

這是我同桌的博客可以互爲註解
lzj之博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章