CF1882E1+CF1882E2 Two Permutations 题解-构造题
CF1882E1+CF1882E2 Two Permutations 题解-构造题
哇,人类智慧,太智慧了。。。
此题作为20231010联考的 T3。
感觉赛时根本没有去往这方面想。
E1 是简单版,就是没有要求操作数最小化。
E1-Easy Version
方法 1
首先考虑没有次数限制的,对于单独的每一个串的情况。
发现每次操作就是要交换前后两个序列——
变化好大,基本上每个位置的数都要变。
那怎么办呢?
这样大的变化很难去操作,所以我们考虑一些组合:
经过一些变化,我们能不能只交换两个数?
模拟一下,我们得到了一下方案:
其中 \(A,B,C\) 是串,\(1,2\) 代表两个数字 \(\to\)
于是我们实现啦~
于是,我们就可以使用 \(n-1\) 次交换使得数组有序,就是在第 \(i\) 个位置找到 \(i\) 这个数交换过来。
所以,最多进行 \(3n-3\) 次操作就可以把数组排序。
解决了排序问题之后,我们考虑如何把两个排序序列调整至相同长度。
那么一定是少的序列做了一些操作使得序列仍有序。
什么操作呢?
手玩一下可以发现其实就是两种:
- 两次为一个循环节,第一次选 \(1\) ,第二次选 \(n\) (
很好理解) - 每次选 \(n\) ,进行 \(n\) 次操作
于是我们设第一个字符串的操作序列是 \(N\) ,第二个序列为 \(M\),不妨令 \(|N| \ge |M|\):
- \(|N|-|M|\) 为偶数,直接用上面的第一种方式放到 \(M\) 的末尾即可。
- \(|N|-|M|\) 为奇数,当 \(n,m\) 中至少又一个奇数的时候,我们对于奇数的那个序列,用第二种构造方式把它变成偶数,再像上面的情况那样做。
而对于 \(n,m\) 都为偶数的情况——好像是没有办法完成的,
因为每一次操作会把逆序对数的奇偶性翻转,所以必须进行偶数次操作才可以。
具体证明是,设 \(A,B\) 为长度 \(a,b\) 的串,\(c\) 是单独的一共数:
交换前 \(AcB\) 的逆序对数:
交换之后得到 \(BcA\) ,逆序对数为:
而由于两两元素不同:
而 \(a+b+1=n\) 是偶数,所以 \(a,b\) 一奇一偶,所以 \(ab+a+b\) 是奇数。
于是每一次都会奇偶翻转,是不可能调整的。
于是 E1 我们就做完啦~
#include <bits/stdc++.h>
using namespace std;
const int N=3e5+5;
struct node{
int org[N],ans[N],cnt=0,n;
}a,b;
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void print(int x,char s){
int p[15],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x){
p[++tmp]=x%10;
x/=10;
}
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar(s);
}
void swp(node &a,int i,int j){
int A=i-1,B=j-i-1,C=a.n-j;
a.ans[++a.cnt]=A+1;
a.ans[++a.cnt]=B+1;
a.ans[++a.cnt]=C+1;
swap(a.org[i],a.org[j]);
}
void sol(node &a){
a.cnt=0;int j;
for(int i=1;i<a.n;i++)
if(a.org[i]!=i){
for(j=i+1;j<=a.n;j++) if(a.org[j]==i) break;
swp(a,i,j);
}
}
void ins(node &a,node &b){//把 a 添加得和 b 一样长
while(a.cnt<b.cnt){
a.ans[++a.cnt]=1;
a.ans[++a.cnt]=a.n;
}
}
void wrk(node &a,node &b){
if(a.cnt<b.cnt) ins(a,b);
else ins(b,a);
}
void update(node &a){
a.cnt+=a.n;
for(int i=a.cnt;i>a.cnt-a.n;i--) a.ans[i]=a.n;
}
int main(){
a.n=read(),b.n=read();
for(int i=1;i<=a.n;i++) a.org[i]=read();
for(int i=1;i<=b.n;i++) b.org[i]=read();
sol(a);sol(b);
if(abs(a.cnt-b.cnt)&1){
if(!(a.n&1)&&!(b.n&1)){puts("-1");return 0;}
if(a.n&1) update(a);
else update(b);
}
wrk(a,b);
print(a.cnt,'\n');
for(int i=1;i<=a.cnt;i++) print(a.ans[i],' '),print(b.ans[i],'\n');
return 0;
}
简单分析一下次数:
令 \(n \gt m\),
首先调整每一个序列需要 \(3n-3\),对于第二种情况还要进行 \(n\) 次调整,所以总次数是 \(4n-3\) 。
方法 2
而 E1 还有一个很妙的方法,和我考场思路挺像的,但我考虑不全。。。
就是每一个我们考虑把 \(i+1\) 换到 \(i\) 的后面,首先需要把 \(i\) 换到最后面,就直接操作 \(i\) 后面的那个点就可以了。
于是我们再对 \(i+1\) 操作就可以把 \(i+1\) 换到 \(i\) 后面了——
太妙了!!!总操作次数不会超过 \(2n\) ,于是考场上的那道题就解决了。
E2-Hard Version
有用的模型
首先引入一个模型:
在一个排列上,要用交换使得排序升序,将每个位置指向位置上面的数应该在的位置,形成一个图,则交换次数至少是 \(n\) 减去环的个数。
也就是说每个长度为 \(len\) 的环,我们至少要 \(len-1\) 次交换。
题解
现在考虑每一次交换,假设是从 \(AB \to BA\),
发现其实就是把原序列循环左移了若干位。
于是我们考虑加入一个数 \(0\) ,
它在序列中表示这个序列从 \(0\) 开始读。
于是对于每一次操作 \(AcB \to BcA\),我们可以改写成:
再稍微转一下后面的式子,(宗旨还是想去交换):
于是操作又变成了和 \(0\) 交换,我们希望询问最小操作次数。
现在我们希望把这个东西换到模型上面取做:
- 如果 \(0\) 在环上面,则需要 \(x-1\) 步操作即可,就每次把 \(0\) 位置应该是的数交换到这里来。
- 如果环不包含 \(0\) ,你就先把 \(0\) 与环上面任意一个数交换,\(0\) 就在环上了。
由模型我们可以知道这一定是最优解。
代码实现好精妙,建议看代码手玩一下!!!
注意到两个问题:
-
由于你加入了 \(0\) ,所以得到的答案序列会有多个:
\[\{0,1,2,3,\dots,n\},\{n,0,1,2,\dots,n-1\},\dots \]所以我们需要枚举得到的是哪一种,而在具体实现中,我们其实可以去枚举初始序列是哪一个,
这样都把它们转成 \(\{ 0,1,2,\dots ,n\}\) 即可。
也就是枚举最开始 \(0\) 在什么位置。
-
由于简单版改变奇偶性肯定不优,所以我们最后记录最小的奇数操作数和最小偶数操作数最后取 \(\min\) 即可
于是按照上面的处理就可以做到时间复杂度 \(O(n^2)\) 了,完全没有问题。
#include <bits/stdc++.h>
using namespace std;
#define pii pair<vector<int>,vector<int> >
#define pb push_back
const int N=3e5+5;
vector<int> a,b;
int n,m,ans=0,res=0;
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void print(int x,char s){
int p[15],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x){
p[++tmp]=x%10;
x/=10;
}
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar(s);
}
pii sol(vector<int> a){
pii res;int n=a.size();
for(int i=0;i<n;i++){
auto b=a;vector<int> tmp;
while(b[0]!=0){//0一开始就在一个环上面
tmp.pb((b[b[0]]-b[0]+n)%n);//从 0 开始看序列
swap(b[b[0]],b[0]);
}
for(int j=1;j<n;j++){//每一次保证了一开始 0 在 0 处
if(b[j]==j) continue;
tmp.pb((b[j]-b[0]+n)%n);//先把 0 交换到环上面
swap(b[j],b[0]);
while(b[0]!=0){
tmp.pb((b[b[0]]-b[0]+n)%n);
swap(b[b[0]],b[0]);
}
}
tmp.pb(1);//最后面加一个数,为了方便后面判 -1
if((int)tmp.size()&1){if(res.first.size()==0||tmp.size()<=res.first.size()) res.first=tmp;}
else{if(res.second.size()==0||tmp.size()<=res.second.size()) res.second=tmp;}
for(int j=0;j<n;j++) a[j]=(a[j]+1)%n;//把原序列循环右移一位
}
return res;
}
int main(){
n=read();m=read();
for(int i=0;i<=n;i++) a.pb(0);
for(int i=0;i<=m;i++) b.pb(0);
for(int i=1,x;i<=n;i++) x=read(),a[x]=i;
for(int i=1,x;i<=m;i++) x=read(),b[x]=i;
pii x=sol(a),y=sol(b);
if(x.first.size()&&y.first.size()) res=1,ans=max(x.first.size(),y.first.size());
if(x.second.size()&&y.second.size()&&(ans==0||max(x.second.size(),y.second.size())<ans)) ans=max(x.second.size(),y.second.size()),res=2;
if(!ans) puts("-1");
else if(ans==1) puts("0");
else{
vector<int> u,v;
if(res==1) u=x.first,v=y.first;
else u=x.second,v=y.second;
u.pop_back();v.pop_back();ans--;
while(u.size()<ans) u.pb(1),u.pb(n);
while(v.size()<ans) v.pb(1),v.pb(m);
print(ans,'\n');
for(int i=0;i<ans;i++) print(u[i],' '),print(v[i],'\n');
}
return 0;
}
Challenge
Codeforces 官网上面还有一个 E1 的加强版,
其实用 E2 的思路完全可以完成。
只是只需要构造一种情况,所以时间复杂度是 \(O(n)\) 的。
Conclusion
- 排序问题的主要思想就是交换,我们希望把题目中的做变成交换。
- 对于每次操作变化很大的题目我们可以考虑找规律把多个操作捆绑,变成交换两个数字去做。
- 排序问题可以往每一次把 \(i+1\) 放到 \(i\) 后面去思考。
- 序列问题涉及交换可以把它转化成环上的问题,即去考虑从哪里开始。
- 注意 if 语句的层层嵌套关系,else 是对于最近的 if 来写的。