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\) 作為階段,\(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\)
狀態數 \(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