CF1620F Bipartite Array 题解

CF1620F Bipartite Array

DP 好题

Statement

多组数据 T2×105

给定一个排列 pn(n106) ,可以任意地取反排列中的任意一个数,即 pi=pi

定义一个排列 p 对应一个无向图 G

  • 如若 i<jpi>pj ,连边 (i,j)

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

对于每次给定的排列,要求构造出一组解使得 p 是二分的

Solution

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

学习自 Sol1 的题解,太神了

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

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

yy 一下,可以知道当且仅当序列可以被拆成两个单增的子序列时,才是二分的。这两个序列无交集,且并起来就是全集。

容易发现刚刚那个丑陋的条件和这个条件其实是充要的关系

因为存在取反的操作,对应到原排列上,肯定是把前面递减的一部分全部取反

所以,需要把原排列拆成 两个单谷子序列

考虑 DP,但不是很会,当时思考的时候仍然保持着“肯定需要知道另外一个序列长成什么样子”的思想,GG

仔细思考一下我们到底需要什么状态

首先,由于最后提出来的子序列无交集,且并起来就是全集,一个元素非此即彼,所以我们只需要关心 ii1 是否在同一个序列里面就可以了

那么肯定需要 i 作为阶段,j=0/1,k=0/1 表示两个序列当前的 降/升 情况

(题外话,Sol1 大佬这里写反了,顺带着后面最大最小值的意义也写反了)

思考转移,发现当前这个包含 i 的子序列要么是和 i1 在一起,不需要额外的信息

但是如若 ii1 不在一起,发现还需要对于 i1 而言另外一个序列的信息( i 所在序列)

啥信息?当然是序列最后一个数是什么啊(粗暴地想

所以初步得到这样一个状态: f[i][j=0/1][k=0/1][v]=0/1 表示当前在 ii 所在序列正在递减/递增,另外一个序列正在递减/递增,另外一个序列最后一个数是 v ;最后一个 0/1 表示这种情况 不会/会 出现

状态数 n2 ,爆掉了,考虑继续压榨状态,仔细思考一下我们到底需要什么状态

发现在 i,j 相同的情况下,若 k=0 ,即另外一个在递减,如若 v1<v2f[i][j][k][v1]=f[i][j][k][v2]=1 ,那么 v2 肯定在后面的构造中更有潜力;若 k=1 ,即另外一个在递增,如若 v1<v2f[i][j][k][v1]=f[i][j][k][v2]=1 ,那么 v1 肯定在后面的构造中更有潜力

具体可以从 LIS/LCS 的角度考虑,让后面发挥空间更大

所以直接把第四位扔进去,设 f[i][j][k] 表示当前在 ii 所在序列正在递减/递增,另外一个序列正在递减/递增,另外一个序列末尾数字的 最大/小 值

(这里 '/' 是对应的)

此时,若 f[n][1][1]>n ,那么无解

然后考虑转移,可能由于蒟蒻是第一次做,所以觉得很神仙,具体在代码里面讲

考虑构造答案,在转移的过程中记录一下 i1i1 有没有在一起就好了。记三个值,分别表示转移点的 j,ki 一定是当前状态的 i 减去 1),以及最后一个数是否与上一个数在同一个子序列里面。可以压成一个 07 中的整数来存储。从 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[i1][0][1] 中增加 else 也要 G

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

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

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

Soltion2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

这样的话,任意奇环上的点 u 的出度减入度之差 du 只能为 22

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

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

好的,我们考虑 DP 构造解

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

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

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

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

一个比较神仙的观察:

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

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

或者,形式化的:不妨设 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(n2)

这里窥见一些端倪!

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

所以直接记 f[i][0/1][0/1]=v 表示前 i 位,第 i 位填的值是正/负,其中 x/y 等于 ±ai+1。然后 v 存的是另外一个数(不等于 ±ai+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 @   _Famiglistimo  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示