1. 程式人生 > 其它 >CF1620F Bipartite Array 題解

CF1620F Bipartite Array 題解

DP 好題

Statement

多組資料 \(T\le 2\times 10^5\)

給定一個排列 \(p_n(n\le 10^6)\) ,可以任意地取反排列中的任意一個數,即 \(p_i=-p_i\)

定義一個排列 \(p\) 對應一個無向圖 \(G\)

  • 如若 \(i<j\)\(p_i>p_j\) ,連邊 \((i,j)\)

定義一個排列 \(p\) 是二分的當且僅當 \(G\) 是一張二分圖

對於每次給定的排列,要求構造出一組解使得 \(p^{\prime}\) 是二分的

Solution

猶豫了一下,還是寫了題解,不然覺得比較可惜。

學習自 Sol1 的題解,太神了

優化一下剛剛那個醜陋的條件,發現對於一個單增的序列而言,內部肯定是不連邊的,即肯定是在同一側

yy 一下,可以知道當且僅當序列可以被拆成兩個單增的子序列時,才是二分的。這兩個序列無交集,且並起來就是全集。

容易發現剛剛那個醜陋的條件和這個條件其實是充要的關係

因為存在取反的操作,對應到原排列上,肯定是把前面遞減的一部分全部取反

所以,需要把原排列拆成 兩個單穀子序列

考慮 DP,但不是很會,當時思考的時候仍然保持著“肯定需要知道另外一個序列長成什麼樣子”的思想,GG

仔細思考一下我們到底需要什麼狀態

首先,由於最後提出來的子序列無交集,且並起來就是全集,一個元素非此即彼,所以我們只需要關心 \(i\)

\(i-1\) 是否在同一個序列裡面就可以了

那麼肯定需要 \(i\) 作為階段,\(j=0/1,k=0/1\) 表示兩個序列當前的 降/升 情況

(題外話,Sol1 大佬這裡寫反了,順帶著後面最大最小值的意義也寫反了)

思考轉移,發現當前這個包含 \(i\) 的子序列要麼是和 \(i-1\) 在一起,不需要額外的資訊

但是如若 \(i\)\(i-1\) 不在一起,發現還需要對於 \(i-1\) 而言另外一個序列的資訊( \(i\) 所在序列)

啥資訊?當然是序列最後一個數是什麼啊(粗暴地想

所以初步得到這樣一個狀態: \(f[i][j=0/1][k=0/1][v]=0/1\) 表示當前在 \(i\)\(i\)

所在序列正在遞減/遞增,另外一個序列正在遞減/遞增,另外一個序列最後一個數是 \(v\) ;最後一個 \(0/1\) 表示這種情況 不會/會 出現

狀態數 \(n^2\) ,爆掉了,考慮繼續壓榨狀態,仔細思考一下我們到底需要什麼狀態

發現在 \(i,j\) 相同的情況下,若 \(k=0\) ,即另外一個在遞減,如若 \(v1<v2\)\(f[i][j][k][v1]= f[i][j][k][v2]=1\) ,那麼 \(v2\) 肯定在後面的構造中更有潛力;若 \(k=1\) ,即另外一個在遞增,如若 \(v1<v2\)\(f[i][j][k][v1]= f[i][j][k][v2]=1\) ,那麼 \(v1\) 肯定在後面的構造中更有潛力

具體可以從 LIS/LCS 的角度考慮,讓後面發揮空間更大

所以直接把第四位扔進去,設 \(f[i][j][k]\) 表示當前在 \(i\)\(i\) 所在序列正在遞減/遞增,另外一個序列正在遞減/遞增,另外一個序列末尾數字的 最大/小 值

(這裡 '/' 是對應的)

此時,若 \(f[n][1][1]>n\) ,那麼無解

然後考慮轉移,可能由於蒟蒻是第一次做,所以覺得很神仙,具體在程式碼裡面講

考慮構造答案,在轉移的過程中記錄一下 \(i-1\)\(i-1\) 有沒有在一起就好了。記三個值,分別表示轉移點的 \(j,k\)\(i\) 一定是當前狀態的 \(i\) 減去 1),以及最後一個數是否與上一個數在同一個子序列裡面。可以壓成一個 \(0\sim 7\) 中的整數來儲存。從 \(n,1,1\) 反著搜一遍就可以得到兩個單穀子序列,分別把前面遞減的部分反過去就可以了。

——Sol1

Code

程式碼確實精髓,時刻牢記我們構造的是單谷

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+6;

char buf[1<<23],*p1=buf,*p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
int read(){
    int s=0,w=1; char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')w=-1; ch=getchar();}
    while(isdigit(ch))s=s*10+(ch^48),ch=getchar();
    return s*w;
}

int f[N][2][2],pre[N][2][2];
int a[N],ans[N];
int T,n;

void updmax(int i,int j,int k,int val,int op){
    //remember the value of op is the previousness
    if(val>f[i][j][k])f[i][j][k]=val,pre[i][j][k]=op;
}
void updmin(int i,int j,int k,int val,int op){
    if(val<f[i][j][k])f[i][j][k]=val,pre[i][j][k]=op;
}

signed main(){
    T=read();
    while(T--){
        n=read();
        for(int i=1;i<=n;++i)
            a[i]=read(),
            f[i][0][0]=f[i][1][0]=0xf3f3f3f3,
            f[i][0][1]=f[i][1][1]=0x3f3f3f3f;
        f[1][0][0]=f[1][1][0]=0x3f3f3f3f;//第一個位置啥事都可以幹
        f[1][0][1]=f[1][1][1]=0xf3f3f3f3;
        for(int i=2;i<=n;++i){
            //建議下面的轉移都畫畫圖
            if(f[i-1][0][0]>=1){
                if(a[i]<a[i-1])updmax(i,0,0,f[i-1][0][0],0);//偶數表示 i i-1 在一起
                // 此時,i 所在遞減,i 可以接在 i-1 後面,直接轉移,取 min
                updmax(i,1,0,f[i-1][0][0],0); 
                // 不管 i-1 和 i 的大小關係,強行抬頭(由減變增),注意因為是單谷,觀察所有的轉移,發現不存在由增到減的強行低頭
                if(a[i]<f[i-1][0][0])updmax(i,0,0,a[i-1],1);//不在一起
                updmax(i,1,0,a[i-1],1);//強行抬頭
            }
            if(f[i-1][1][0]>=1){
                if(a[i-1]<a[i])updmax(i,1,0,f[i-1][1][0],4);//狀態的順承
                //按照強行抬頭的理論,這裡其實可以這樣寫一句 updmin(i,1,1,f[i-1][1][0],6); ,但是問題在於本次轉移是對 i 所在序列操作,另外一個序列根本沒有改變,另外一個序列可能是空等等,而這裡卻強行抬頭,GG
                if(a[i]<f[i-1][1][0])updmin(i,0,1,a[i-1],5);
                //不在一起,注意這裡 0 1 交換位置,因為對於 i-1 的另外一個序列就是 i 所在序列
                //此時 i 所在序列遞減,i-1 所在序列遞增
                updmin(i,1,1,a[i-1],5);//強行抬頭
            }
            if(f[i-1][0][1]<=n){//與上一個同理
                if(a[i-1]>a[i])updmin(i,0,1,f[i-1][0][1],2);
                //只要 k=0 那麼 max,否則 min
                updmin(i,1,1,f[i-1][0][1],2);
                if(a[i]>f[i-1][0][1])updmax(i,1,0,a[i-1],3);
            }
            if(f[i-1][1][1]<=n){//都是順承
                if(a[i-1]<a[i])updmin(i,1,1,f[i-1][1][1],6);
                if(f[i-1][1][1]<a[i])updmin(i,1,1,a[i-1],7);
            }
        }
        
        if(f[n][1][1]>n){
            puts("NO");
            continue;
        }
        puts("YES");
        vector<int>seq[2];
        for(int i=n,j=1,k=1,cur=0,nex;i>=1;--i)
            ans[i]=a[i],nex=pre[i][j][k],seq[cur].push_back(i),
            cur^=(nex&1),j=(nex>>2)&1,k=(nex>>1)&1;
        for(int k=0;k<2;++k){
            reverse(seq[k].begin(),seq[k].end());
            for(int i=0;i<(int)seq[k].size()-1;++i)//前一段遞減,後一段遞增
                if(a[seq[k][i]]>a[seq[k][i+1]])ans[seq[k][i]]=-a[seq[k][i]];
                else break;
        }
        for(int i=1;i<=n;++i)
            printf("%d ",ans[i]);
        puts("");
    }
    return 0;
}

現在還剩下一個神奇的問題,問題的發現是在沒有發現 Sol1 大佬狀態定義寫反之前發現的,如若像這樣寫:

if(f[i-1][0][0]>=1){
    if(a[i]<a[i-1])updmax(i,0,0,f[i-1][0][0],0);
    else /*************/updmax(i,1,0,f[i-1][0][0],0); 
    if(a[i]<f[i-1][0][0])updmax(i,0,0,a[i-1],1);
    else /*************/updmax(i,1,0,a[i-1],1);
}

是對的,即放棄強制抬頭的決定,正常抬頭,然而

if(f[i-1][1][0]>=1){
   if(a[i-1]<a[i])updmax(i,1,0,f[i-1][1][0],4);
   if(a[i]<f[i-1][1][0])updmin(i,0,1,a[i-1],5);
   else updmin(i,1,1,a[i-1],5);
   // why the correction of the code is depend on if I add 'else' before
}

就要 G,當然在下面 \(f[i-1][0][1]\) 中增加 else 也要 G

粗淺地猜測一下,觀察 \(f[i][1][1]\) 的轉移,發現如若不強行抬頭,可能根本沒有機會(全是單增/單減也是允許的)

而前面那個抬不抬無所謂啦,畢竟就算是不認為在抬頭,該抬頭還是要抬

但還是停留在猜測階段,具體正在私信 Sol1 大佬/hanx