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\(p_i>p_j\) ,连边 \((i,j)\)

定义一个排列 \(p\) 是二分的当且仅当 \(G\) 是一张二分图

对于每次给定的排列,要求构造出一组解使得 \(p^{\prime}\) 是二分的

Solution

犹豫了一下,还是写了题解,不然觉得比较可惜。

学习自 Sol1 的题解,太神了

简单地推了下性质,推出一个比较垃圾的条件,就是说每一个数前面比他大的数必须长成单调递增的样子,然后尝试 DP,说那么我肯定需要知道前面的序列长成什么样子,这个根本做不了嘛

优化一下刚刚那个丑陋的条件,发现对于一个单增的序列而言,内部肯定是不连边的,即肯定是在同一侧

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\(f[i][j][k][v1]= f[i][j][k][v2]=1\) ,那么 \(v2\) 肯定在后面的构造中更有潜力;若 \(k=1\) ,即另外一个在递增,如若 \(v1\(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
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=1){
                if(a[i]=1){
                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]n){
            puts("NO");
            continue;
        }
        puts("YES");
        vectorseq[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]

是对的,即放弃强制抬头的决定,正常抬头,然而

if(f[i-1][1][0]>=1){
   if(a[i-1]

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

粗浅地猜测一下,观察 \(f[i][1][1]\) 的转移,发现如若不强行抬头,可能根本没有机会(全是单增/单减也是允许的)

而前面那个抬不抬无所谓啦,毕竟就算是不认为在抬头,该抬头还是要抬

但还是停留在猜测阶段,具体正在私信 Sol1 大佬/hanx

Soltion2

看完 GaryH 的题解没忍住,觉得其中不断缩减 DP 状态,对我们到底需要什么状态的思考非常巧妙

(下面引起来的内容都是贺的)

此处,我们把最初那个丑陋的条件再看一下:

就是说每一个数前面比他大的数必须长成单调递增的样子

这里推成了另外一种等价的形式:

对于当前数 \(i\) ,假装我们前面的数都构造出来了,所有逆序对末尾数最大为 \(y\),我们只需要 \(val(i)>y\) 即可。

容易发现这个条件和刚刚那个是等价的,只不过是从反面说而已

在题解中,表述成了这个样子:

故我们让图中不存在长为 3 的奇环,就可以满足条件。

容易发现这个表述和上面的做法是等价的,浅证一下:

只需证明在本题中 有奇环 \(\Leftrightarrow\) 存在长为 3 的奇环即可

反证法,若图中存在一个长度大于 3 的奇环但不存在长度为 3 的奇环

把原本的无向边假想为有向边,从大的那边连向小的那边

考虑奇环中有公共端点的两条边,容易发现这两条边要么同时指向这个公共端点,要么同时从这个公共端点指向其他点

这样的话,任意奇环上的点 \(u\) 的出度减入度之差 \(d_u\) 只能为 \(2\)\(-2\)

则一个奇环上所有点的 \(d\) 值之和必然不为零,产生矛盾

所以假设不成立,所以有奇环 \(\Leftrightarrow\) 存在长为 3 的奇环

好的,我们考虑 DP 构造解

考虑我们需要知道什么东西,考虑到我们填入这个数之后可能会产生新的逆序对

所以我们还需要保存之前构造的解的最大值

粗暴地,我们这样设出方程: \(f[i][x][y]=1/0\) ,前 \(i\) 个,最大值 \(x\) ,最小的逆序对末尾数为 \(y\) ,是否可行

状态数爆炸,考虑优化状态数

一个比较神仙的观察:

我们发现,对于两个都已经构造前了 \(i\) 位的序列 \(p1,p2\),若两者前 \(i\) 位的最大数都为 \(x\)

设两者的逆序对末尾数最大值分别为 \(y1,y2\),显然,在 \(y1\)\(y2\) 中取小者对应的序列更有潜力,

或者,形式化的:不妨设 \(y1>y2\),则任意一个长为 \(n\) 的合法序列,

若其长为 \(i\) 的前缀由 \(p1\) 构成,则我们将前缀换成 \(p2\) 后也一定合法。

同样的,对于一对固定的 \((i,y)\),显然 \(x\) 越小序列越容易合法,就不过多赘述了。

所以,就像 Solution1 中所干的事情,我们改设 \(f[i][x]=y\) 表示最大值为 \(x\) 情况下最小 \(y\)

容易写出 \(O(1)\) 转移,如若当前的数 \( 那么显然直接 GG,

如若当前的数 \(>x\) ,转移到 \((v,y)\) ,否则转移到 \((x,v)\)

但是还是过不了,主要矛盾还是在状态数的 \(O(n^2)\)

这里窥见一些端倪!

发现一个奇怪的现象,转移后 \((x,y)^{\prime}\) 中必然有一个数是 \(v\)

所以直接记 \(f[i][0/1][0/1]=v\) 表示前 \(i\) 位,第 \(i\) 位填的值是正/负,其中 \(x/y\) 等于 \(\pm a_{i+1}\)。然后 \(v\) 存的是另外一个数(不等于 \(\pm a_{i+1}\) 的那个数)的值

\(O(n)\) !!!

Code2

#include
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;
}
bool cmin(int &a,int b){return a>b?a=b,1:0;}
bool cmax(int &a,int b){return a=x&&cmin(f[i+1][sgn][0],y))
                        pre[i+1][sgn][0]=j*2+k;//状态压缩一下
                    if(zans;
        for(int j=0;j<2;++j)
            for(int k=0;k<2;++k)
                if(f[n][j][k]<2e9){
                    puts("YES");
                    for(int i=n,t;i;--i){
                        ans.push_back(j?a[i]:-a[i]);
                        t=pre[i][j][k];
                        j=(t>>1)&1,k=t&1;
                    }
                    reverse(ans.begin(),ans.end());
                    for(auto v:ans)printf("%d ",v);
                    puts("");
                    goto r;
                }
        puts("NO");
        r:;
    }
    return 0;
}
DP