1. 程式人生 > 實用技巧 >「HAOI 2006」數字序列

「HAOI 2006」數字序列

Description

給定一長度為 \(n\) 的數列 \(a\),可將 \(a_i\) 改為任意整數 \(k\),代價為 \(\mid a_i-k\mid\)

問最少改變多少個數能把它變成一個單調嚴格上升的序列。

輸出最少需要改變的數的個數,以及在改變的數最少的情況下,最小的代價和。

\(1\leq n\leq 3.5\times 10^4,1\leq a_i\leq 10^5\)

Solution

Part 1

Solve Problem 1:需要改變的數最少,則需保留的數要儘可能多。考慮取一個補集,問題轉化為求最多保留多少個數。

對於兩個數 \(a_i,a_j\)(不妨設 \(i<j\)

),若可同時保留 \(i\)\(j\),則 \(a_i,a_j\) 需滿足:

  • \(a_i<a_j\)(顯然)。

  • 改變 \([i+1,j-1]\) 內的數能夠使 \([i,j]\) 嚴格單調上升。所以 \(a_j-a_i\geq j-i\)。移項可得,\(a_i-i\leq a_j-j\)

構造數列 \(b_i=a_i-i\),問題轉化為求 \(b\) 的最長不降子序列。可 \(O(n\log n)\) 求得。

Part 2

Solve Problem 2:使 \(a\) 單調上升的代價,就是使 \(b\) 單調不降的代價。

考慮在 \(b\) 的最長不降子序列中,任意兩個相鄰的元素。設它們 \(b\)

中的位置分別為 \(l,r\),則一定 不存在 \(i\in[l,r]\),使得 \(b_l\leq b_i\leq b_r\)。否則取上 \(i\),保證合法,而且可以使最長不降子序列更長。

所以對於 \(\forall i\in[l,r]\)\(b_i<b_l\)\(b_i>b_r\)

考慮如何改變 \(b_i\) 的值,能使序列合法且代價和最小。

結論:存在一個數 \(k\in [l,r]\):對於 \(\forall i\in[l,k]\),把 \(b_i\) 改成 \(b_l\)。對於 \(\forall i\in[k+1,r]\),把 \(b_i\) 改成 \(b_r\)

。此時代價和最小。

假設 \([l,r]\) 之間有 \(n\) 個數。

  • \(n=1\) 時,結論顯然成立。(因為 \(b_i<b_l\)\(b_i>b_r\)\(b_i\) 改為 \(b_l\)\(b_r\) 顯然比改為取值在 \([b_l,b_r]\) 之間的數優)

  • \(n>1\) 時:

    • \(n-1\) 個數一半改為 \(b_l\) 一半改為 \(b_r\):當 \(b_n>b_r\) 時,顯然將 \(b_n\) 改為 \(b_r\) 比較優。當 \(b_n<b_l\) 時,若 \(b_n\) 不改為 \(b_r\) 改為了 \(b_l+k\)\(0\leq k\leq b_r-b_l\)),為了使序列單調不降,前面所有改為 \(b_r\) 的數都應改成 \(b_l+k\)(設這樣的數有 \(x\) 個)。\(x\times (b_r-(b_l+k))+((b_l+k)-b_n)=xb_r-(x-1)(b_l+k)-b_n\geq b_r-b_n\),所以此時 \(b_n\) 改為 \(b_r\) 更優。

    • \(n-1\) 個數全改為 \(b_l\)\(b_r\):略。

Part 3

\({dp}_i\) 表示最後一位是\(b_i\)時單調不降的最小代價。

列舉 \(j\),列舉的 \(j\) 需滿足:

  • \(j<i,b_j<b_i\)

  • \(b_j\) 結尾的最長不降子序列長度 \(=\)\(b_i\) 結尾的 \(-1\)

列舉分界點 \(k\),有:

\(\displaystyle{dp}_i=\min\{{dp}_j+\sum\limits_{p=j+1}^k\mid b_p-b_j\mid+\sum\limits_{p=k+1}^{i-1}\mid b_p-b_i\mid\}\)

即:對於 \(p\in[j+1,k]\),將 \(b_p\) 改為 \(b_j\)。對於 \(p\in[k+1,i-1]\),將 \(b_p\) 改為 \(b_i\)

字首和優化轉移即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e4+5;
int n,a[N],b[N],len,f[N],g[N],dp[N],pre[N],suf[N];
vector<int>v[N];    //v[i]: 記錄長度為 i 的最長不降子序列的結尾
void solve(int l,int r,int L,int R){
    pre[l]=suf[r+1]=0;
    for(int i=l+1;i<=r;i++)    //字首和 
        pre[i]=pre[i-1]+abs(b[i]-L);
    for(int i=r;i>=l+1;i--)    //字尾和 
        suf[i]=suf[i+1]+abs(b[i]-R); 
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]),b[i]=a[i]-i;
    b[0]=-1e9,b[++n]=1e9;     //邊界。加上前後最小值最大值方便操作。最大值不要設太大,不然算字首和的時候可能會爆。 
    f[1]=b[1],len=1,g[1]=1,v[1].push_back(1); 
    for(int i=2;i<=n;i++){    //O(n log n) 求最長不降子序列 
        if(b[i]>=f[len]) f[++len]=b[i],g[i]=len;
        else{
            int x=upper_bound(f+1,f+1+len,b[i])-f;
            f[x]=b[i],g[i]=x;    //g[i]: 以第 i 個數結尾的最長不降子序列的長度 
        }
    }
    for(int i=0;i<=n;i++) v[g[i]].push_back(i); 
    memset(dp,0x3f,sizeof(dp)),dp[0]=0;
    for(int i=1;i<=n;i++)
        for(int p=0;p<(int)v[g[i]-1].size();p++){    //如果 b[j] 要拼上前面合適的 b[i],就去前面找長度為 g[i]-1 且能拼上的 
            int j=v[g[i]-1][p];        //以 b[j] 結尾的 最長不降子序列長度 = 以 b[i] 結尾的 -1 
            if(j>i||b[j]>b[i]) continue;    //j<i,b[j]<=b[i] 才行 
            solve(j,i,b[j],b[i]);  
            for(int k=j;k<i;k++)    //列舉分界點 k 
                dp[i]=min(dp[i],dp[j]+pre[k]+suf[k+1]);
        }
    printf("%lld\n%lld\n",n-len,dp[n]);
    return 0;
}