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 的题解,太神了

简单地推了下性质,推出一个比较垃圾的条件,就是说每一个数前面比他大的数必须长成单调递增的样子,然后尝试 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<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

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)\) 转移,如若当前的数 \(<y\) 那么显然直接 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<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;
}
bool cmin(int &a,int b){return a>b?a=b,1:0;}
bool cmax(int &a,int b){return a<b?a=b,1:0;}

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

signed main(){
    T=read();
    while(T--){
        n=read();
        for(int i=1;i<=n;++i)
            a[i]=read(),f[i][0][0]=f[i][0][1]=f[i][1][0]=f[i][1][1]=2e9;
        f[1][0][0]=f[1][1][0]=f[1][1][1]=f[1][0][1]=-2e9;
        for(int i=1,x,y;i<n;++i)
            for(int j=0;j<2;++j)for(int k=0;k<2;++k){
                if(j&&k)y=a[i],x=f[i][j][k];
                if(j&&!k)x=a[i],y=f[i][j][k];
                if(!j&&k)y=-a[i],x=f[i][j][k];
                if(!j&&!k)x=-a[i],y=f[i][j][k];

                for(int sgn=0,z;sgn<2;++sgn){
                    z=(sgn?1:-1)*a[i+1];
                    if(z<y)continue;
                    if(z>=x&&cmin(f[i+1][sgn][0],y))
                        pre[i+1][sgn][0]=j*2+k;//状态压缩一下
                    if(z<x&&cmin(f[i+1][sgn][1],x))
                        pre[i+1][sgn][1]=j*2+k;
                }
            }
        vector<int>ans;
        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;
}
posted @ 2022-05-17 16:06  _Famiglistimo  阅读(38)  评论(0编辑  收藏  举报