CF882E1+CF1882E2 Two Permutations 题解-构造题

发布时间 2023-10-11 23:30:29作者: H_W_Y

CF882E1+CF1882E2 Two Permutations 题解-构造题

哇,人类智慧,太智慧了。。。

此题作为20231010联考的 T3。

感觉赛时根本没有去往这方面想。

CF1882E1 CF1882E2

E1 是简单版,就是没有要求操作数最小化。

E1-Easy Version

方法 1

首先考虑没有次数限制的,对于单独的每一个串的情况。

发现每次操作就是要交换前后两个序列——

变化好大,基本上每个位置的数都要变。

那怎么办呢?

这样大的变化很难去操作,所以我们考虑一些组合:

经过一些变化,我们能不能只交换两个数

模拟一下,我们得到了一下方案:

其中 \(A,B,C\) 是串,\(1,2\) 代表两个数字 \(\to\)

\[A1B2C\to 选1 \to B2C1A \to 选2 \to C1A2B \to 选1 \to A2B1C \]

于是我们实现啦~

于是,我们就可以使用 \(n-1\) 次交换使得数组有序,就是在第 \(i\) 个位置找到 \(i\) 这个数交换过来。

所以,最多进行 \(3n-3\) 次操作就可以把数组排序。


解决了排序问题之后,我们考虑如何把两个排序序列调整至相同长度。

那么一定是少的序列做了一些操作使得序列仍有序。

什么操作呢?

手玩一下可以发现其实就是两种:

  1. 两次为一个循环节,第一次选 \(1\) ,第二次选 \(n\)很好理解
  2. 每次选 \(n\) ,进行 \(n\) 次操作

于是我们设第一个字符串的操作序列是 \(N\) ,第二个序列为 \(M\),不妨令 \(|N| \ge |M|\)

  1. \(|N|-|M|\) 为偶数,直接用上面的第一种方式放到 \(M\) 的末尾即可。
  2. \(|N|-|M|\) 为奇数,当 \(n,m\) 中至少又一个奇数的时候,我们对于奇数的那个序列,用第二种构造方式把它变成偶数,再像上面的情况那样做。

而对于 \(n,m\) 都为偶数的情况——好像是没有办法完成的,

因为每一次操作会把逆序对数的奇偶性翻转,所以必须进行偶数次操作才可以。

具体证明是,设 \(A,B\) 为长度 \(a,b\) 的串,\(c\) 是单独的一共数:

交换前 \(AcB\) 的逆序对数:

\[Lst=\sum\limits_{i \in A} \sum \limits_{j \in B} [i \gt j] + \sum\limits_{i \in A} [i \gt c] + \sum\limits_{i \in B} [i \lt C] \]

交换之后得到 \(BcA\) ,逆序对数为:

\[Nw=\sum\limits_{i \in A} \sum \limits_{j \in B} [i \lt j] + \sum\limits_{i \in A} [i \lt c] + \sum\limits_{i \in B} [i \gt C] \]

而由于两两元素不同:

\[\begin{align*} Nw & =(ab-\sum\limits_{i \in A} \sum \limits_{j \in B} [i \gt j]) +(a- \sum\limits_{i \in A} [i \gt c]) + (b-\sum\limits_{i \in B} [i \lt C])\\ &=ab+a+b-Lst \end{align*} \]

\(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\),我们可以改写成:

\[0AcB\to 0BcA \]

再稍微转一下后面的式子,(宗旨还是想去交换):

\[0AcB\to cA0B \]

于是操作又变成了和 \(0\) 交换,我们希望询问最小操作次数。


现在我们希望把这个东西换到模型上面取做:

  1. 如果 \(0\) 在环上面,则需要 \(x-1\) 步操作即可,就每次把 \(0\) 位置应该是的数交换到这里来。
  2. 如果环不包含 \(0\) ,你就先把 \(0\) 与环上面任意一个数交换,\(0\) 就在环上了。

由模型我们可以知道这一定是最优解。

代码实现好精妙,建议看代码手玩一下!!!


注意到两个问题:

  1. 由于你加入了 \(0\) ,所以得到的答案序列会有多个:

    \[\{0,1,2,3,\dots,n\},\{n,0,1,2,\dots,n-1\},\dots \]

    所以我们需要枚举得到的是哪一种,而在具体实现中,我们其实可以去枚举初始序列是哪一个,

    这样都把它们转成 \(\{ 0,1,2,\dots ,n\}\) 即可。

    也就是枚举最开始 \(0\) 在什么位置。

  2. 由于简单版改变奇偶性肯定不优,所以我们最后记录最小的奇数操作数和最小偶数操作数最后取 \(\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

  1. 排序问题的主要思想就是交换,我们希望把题目中的做变成交换。
  2. 对于每次操作变化很大的题目我们可以考虑找规律把多个操作捆绑,变成交换两个数字去做。
  3. 排序问题可以往每一次把 \(i+1\) 放到 \(i\) 后面去思考。
  4. 序列问题涉及交换可以把它转化成环上的问题,即去考虑从哪里开始。
  5. 注意 if 语句的层层嵌套关系,else 是对于最近的 if 来写的。