CF1620F Bipartite Array 题解
CF1620F Bipartite Array
DP 好题
Statement
多组数据
给定一个排列 ,可以任意地取反排列中的任意一个数,即
定义一个排列 对应一个无向图 :
- 如若 且 ,连边
定义一个排列 是二分的当且仅当 是一张二分图
对于每次给定的排列,要求构造出一组解使得 是二分的
Solution
犹豫了一下,还是写了题解,不然觉得比较可惜。
学习自 Sol1 的题解,太神了
简单地推了下性质,推出一个比较垃圾的条件,就是说每一个数前面比他大的数必须长成单调递增的样子,然后尝试 DP,说那么我肯定需要知道前面的序列长成什么样子,这个根本做不了嘛
优化一下刚刚那个丑陋的条件,发现对于一个单增的序列而言,内部肯定是不连边的,即肯定是在同一侧
yy 一下,可以知道当且仅当序列可以被拆成两个单增的子序列时,才是二分的。这两个序列无交集,且并起来就是全集。
容易发现刚刚那个丑陋的条件和这个条件其实是充要的关系
因为存在取反的操作,对应到原排列上,肯定是把前面递减的一部分全部取反
所以,需要把原排列拆成 两个单谷子序列
考虑 DP,但不是很会,当时思考的时候仍然保持着“肯定需要知道另外一个序列长成什么样子”的思想,GG
仔细思考一下我们到底需要什么状态
首先,由于最后提出来的子序列无交集,且并起来就是全集,一个元素非此即彼,所以我们只需要关心 和 是否在同一个序列里面就可以了
那么肯定需要 作为阶段, 表示两个序列当前的 降/升 情况
(题外话,Sol1 大佬这里写反了,顺带着后面最大最小值的意义也写反了)
思考转移,发现当前这个包含 的子序列要么是和 在一起,不需要额外的信息
但是如若 和 不在一起,发现还需要对于 而言另外一个序列的信息( 所在序列)
啥信息?当然是序列最后一个数是什么啊(粗暴地想
所以初步得到这样一个状态: 表示当前在 , 所在序列正在递减/递增,另外一个序列正在递减/递增,另外一个序列最后一个数是 ;最后一个 表示这种情况 不会/会 出现
状态数 ,爆掉了,考虑继续压榨状态,仔细思考一下我们到底需要什么状态
发现在 相同的情况下,若 ,即另外一个在递减,如若 , ,那么 肯定在后面的构造中更有潜力;若 ,即另外一个在递增,如若 , ,那么 肯定在后面的构造中更有潜力
具体可以从 LIS/LCS 的角度考虑,让后面发挥空间更大
所以直接把第四位扔进去,设 表示当前在 , 所在序列正在递减/递增,另外一个序列正在递减/递增,另外一个序列末尾数字的 最大/小 值
(这里 '/' 是对应的)
此时,若 ,那么无解
然后考虑转移,可能由于蒟蒻是第一次做,所以觉得很神仙,具体在代码里面讲
考虑构造答案,在转移的过程中记录一下 和 有没有在一起就好了。记三个值,分别表示转移点的 ( 一定是当前状态的 减去 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,当然在下面 中增加 else 也要 G
粗浅地猜测一下,观察 的转移,发现如若不强行抬头,可能根本没有机会(全是单增/单减也是允许的)
而前面那个抬不抬无所谓啦,毕竟就算是不认为在抬头,该抬头还是要抬
但还是停留在猜测阶段,具体正在私信 Sol1 大佬/hanx
Soltion2
看完 GaryH 的题解没忍住,觉得其中不断缩减 DP 状态,对我们到底需要什么状态的思考非常巧妙
(下面引起来的内容都是贺的)
此处,我们把最初那个丑陋的条件再看一下:
就是说每一个数前面比他大的数必须长成单调递增的样子
这里推成了另外一种等价的形式:
对于当前数 ,假装我们前面的数都构造出来了,所有逆序对末尾数最大为 ,我们只需要 即可。
容易发现这个条件和刚刚那个是等价的,只不过是从反面说而已
在题解中,表述成了这个样子:
故我们让图中不存在长为 3 的奇环,就可以满足条件。
容易发现这个表述和上面的做法是等价的,浅证一下:
只需证明在本题中 有奇环 存在长为 3 的奇环即可
反证法,若图中存在一个长度大于 3 的奇环但不存在长度为 3 的奇环
把原本的无向边假想为有向边,从大的那边连向小的那边
考虑奇环中有公共端点的两条边,容易发现这两条边要么同时指向这个公共端点,要么同时从这个公共端点指向其他点
这样的话,任意奇环上的点 的出度减入度之差 只能为 或
则一个奇环上所有点的 值之和必然不为零,产生矛盾
所以假设不成立,所以有奇环 存在长为 3 的奇环
好的,我们考虑 DP 构造解
考虑我们需要知道什么东西,考虑到我们填入这个数之后可能会产生新的逆序对
所以我们还需要保存之前构造的解的最大值
粗暴地,我们这样设出方程: ,前 个,最大值 ,最小的逆序对末尾数为 ,是否可行
状态数爆炸,考虑优化状态数
一个比较神仙的观察:
我们发现,对于两个都已经构造前了 位的序列 ,若两者前 位的最大数都为
设两者的逆序对末尾数最大值分别为 ,显然,在 和 中取小者对应的序列更有潜力,
或者,形式化的:不妨设 ,则任意一个长为 的合法序列,
若其长为 的前缀由 构成,则我们将前缀换成 后也一定合法。
同样的,对于一对固定的 ,显然 越小序列越容易合法,就不过多赘述了。
所以,就像 Solution1 中所干的事情,我们改设 表示最大值为 情况下最小
容易写出 转移,如若当前的数 那么显然直接 GG,
如若当前的数 ,转移到 ,否则转移到
但是还是过不了,主要矛盾还是在状态数的
这里窥见一些端倪!
发现一个奇怪的现象,转移后 中必然有一个数是
所以直接记 表示前 位,第 位填的值是正/负,其中 等于 。然后 存的是另外一个数(不等于 的那个数)的值
!!!
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】