Codeforces Round #797 (Div. 3) F(字符串循环节)

题意:给定一个字符串s,以及一个排列p,每次操作可以将字符串按照排列顺序置换一次,比如s=wmbe p=[3,1,4,2] 操作后 s=s3s1s4s2=bwem。输出将该字符串变为原串需要的最小次数。

这道题有暴力做法也有比较结论性的做法。

我们先引进环的概念。我们知道对于任意两个排列,我们可以形象出环,比如排列p1=[1,2,3,4],p2=[2,1,4,3],我们可以表示为1->2,2->1,3->4,4->3这四条边,所以我们可以画出两个环。有了环的概念我们就能得到一个置换时很好的性质。

这里为了方便理解我们给出一个环p1[1,2,3,4],p2[3,1,4,2],边为1->3,2->1,3->4,4->2,于是我们可以得到这个环为1->3->4->2->1,接下来我们再一次进行置换操作,则此时原本的边会变成1->3->4,2->1->3,3->4->2,4->2->1,此时对应这一次置换相当于在这个环上右移一格,

于是我们可以发现每一次置换操作都能对应到当前环上去。最多循环4次之后环又和原来一样,也就是恢复到原来的排列。

当把这些排列变成字符的时候,对应到环上的则是一个字符串环,我们知道排列是互不相同的,所以只有经过排列个数次置换后才能变为原来的排列,而对于字符串环来说,我们能提前就找到这个循环。

对于这样一个字符串环我们不难发现他只有两种状态,即循环两次就能达到原来状态。

怎么处理出环呢?既然我们已经将数拓展到图上的环,就可以用类型邻接表的形式把所有环处理出来。

代码:

for(int i=1;i<=n;i++){
        	if(vis[i])continue;//如果这个节点被访问过就跳过
			//t存储字符串环
			int m=0;//m为字符串长度 
			for(int j=i;!vis[j];j=p[j]){
				vis[j]=1;
				s1[++m]=s[j];//s1为字符串环 
			} 
}

那对于不同的字符串环,我们应该怎么找到他的最小循环次数呢。

这就引出了循环节和最小循环节的概念。

顾名思义,循环节就是他达到原本状态所需要的循环次数,而最小循环节则是最小的循环次数,

比如上图,循环节可以时2,4,6,即2次,4次,6次置换都可以变为原来的环。

那么我们怎么求循环节呢?

我们可以暴力对环进行操作,即圆环中字符变为原来的字符的所需的次数,再判断所有字符是否都能经过该次数变为原来的字符,若能就能保证这个环在进行该次数后能变为原来的环,这样的思路复杂度为o(n^2)。

但是这显然不够优美,那我们就再给一个o(nlogn)的做法吧:

字符串的循环节是可以nlogn求出来的,设这个字符串s的长度为n,,则它最多经过n次置换后变为原串.

所有我们枚举1-n次置换:设当前枚举k次置换

代码:


	int res=0;//res代表循环节长度 
	for(int k=1;k<=n;k++){
		if(n%k==0){//只有n%k==0的情况下才有循环节 
			//因为n要刚好分成n/k段相同的数 
			bool flag=1;
			for(int i=0;i<n;i++){
				if(s[(a+k)%cnt]!=s[a]){
					flag=0;//s[(a+k)%cnt]表示进行k次操作后这个字符为什么 
					break; 
				}
			}
			if(flag){//找到最小循环节即退出 
				ans=k;
				break;
			} 
		}
	}

 你以为这样就结束了?我们还能用kmp算法在o(n)时间内线性求出循环节

 代码:


   for(int i=2,j=0;i<=n;i++){
      while(j &&p[i]!=p[j+1])j=nx[j];
 	  if(p[i]==p[j+1])j++;
 	  nx[i]=j;
   }
   int x=n-nx[n],res;//res是循环节长度 
   if(n%x==0){
   	  res=x;
   }
   else res=n;//没有循环节

到此为止我们就把前置知识都将完了,那这个题也就很容易了

思路:处理出所有的字符串环,因为所有环之间的置换互不相干,所以只要求出所有环的最小循环节再对所有循环节取最小公倍数就能保证一定能变回原来的字符串。

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
const int N=2e5+10;
int p[N],vis[N],nx[N];
char s1[N],s[N]; 
ll gcd(ll a,ll b){
	if(b==0)return a;
	else return gcd(b,a%b);
}
ll lcm(ll a,ll b){
	return a/gcd(a,b)*b;
}
int main(){
	int t;
	cin>>t;
	while(t--){
		int n;
		cin>>n;
		cin>>s+1; //kmp从1开始处理 
		for(int i=1;i<=n;i++){
			cin>>p[i];//输入排列
			vis[i]=0;//初始化为全部没有访问过 
		} 
		ll ans=1; 
        for(int i=1;i<=n;i++){
        	if(vis[i])continue;//如果这个节点被访问过就跳过
			//t存储字符串环
			int m=0;//m为字符串长度 
			for(int j=i;!vis[j];j=p[j]){
				vis[j]=1;
				s1[++m]=s[j];//s1为字符串环 
			} 
			//for(int i=1;i<=m;i++)cout<<s1[i];
			//cout<<endl; 
			for(int i=2,j=0;i<=m;i++){//处理出nx数组 
				while(j &&s1[i]!=s1[j+1])j=nx[j];
				if(s1[i]==s1[j+1])j++;
				nx[i]=j;
			}
			ll x=m-nx[m];
			if(m%x==0)ans=lcm(ans,x);//整除循环节为x 
			else ans=lcm(ans,m); //不能整除循环节为m 
		}
		cout<<ans<<endl;
	}
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值