【做题】atc_cf17-final_E - Combination Lock——巧妙转化及图论
题意:给出一个由26个小写字母组成的字符串,可以任意地进行若干个操作,每次操作是让指定区间内的字母变为下一个字母(z变成a)。问是否存在方案使得这个字符串变为回文串。
一开始的想法是构造len/2条模26意义下方程,但由于26不是质数,判断有没有解并不容易。(下文自动省略模26)
我们发现,一次操作对于方程组的影响大概是这样的:
如上图所示,一次对区间[l,r]进行的操作,其中从一端到其关于中点的对称点的区间实质是没有贡献的。(上图中为区间[l,l'])同样地,对于完全位于中点右边的区间,可以将其翻转到中点左边。
因此,所有操作的影响都被移动到了中点左边,则每一条方程都变成了如下形式:k1*x1+k2*x2+...+kn*xn=p。其中,p为常数,且ki为0或1。
而且,每一次操作的影响都是一个区间,所以这就成了一个通过区间操作是一个序列中元素全部变为0的问题。
上述问题简单地说就是,回文串就是左右两边的元素差值为0。所以最终目的就是使这些差值全部变为0。
所以很容易想到对于那个需要变成0的数列差分得到序列a,每一次操作区间[l,r]就是a[l]+1,a[r+1]-1。这等价于令a[l]+a[r+1]一定,然后任意确定a[l]和a[r+1]的值。
同样地,两个操作[l1,r-1]和[l2,r-1]也就是a[l1]+a[l2]+a[r]一定,然后任意确定这三个数的值。
因此,我们可以建一张有len/2+1个点的图,点i的权值为a[i],每一次操作[l,r]就是给点l和点r+1连一条边。则存在方案 <=> 每个连通分量内点权和为0。
上述过程可以用并查集实现。
时间复杂度O(n*α(len))。(下面代码没有按秩合并,时间复杂度为O(n*logn);有代码更优美的实现方式,即计算时保留S的右半部分)
1 #include <bits/stdc++.h> 2 using namespace std; 3 const int N = 100010; 4 int n,m,s[N],sum[N],dif[N]; 5 int flag[N]; 6 int get_flag(int pos) { 7 return flag[pos] == pos ? pos : flag[pos] = get_flag(flag[pos]); 8 } 9 void fit(int& x) { 10 if (x > n/2) x = n-x+1; 11 } 12 int fix(int x) { 13 if (x<0) x = 26-(-x)%26; 14 x %= 26; 15 return x; 16 } 17 int main() { 18 int l,r; 19 for (char tmp=getchar();tmp>='a'&&tmp<='z';tmp=getchar()) 20 s[++n] = tmp-'a'; 21 for (int i=1;i<=n/2;++i) 22 s[i] -= s[n-i+1],dif[i]=s[i]-s[i-1],flag[i]=i; 23 dif[n/2+1] = -s[n/2]; 24 scanf("%d",&m); 25 for (int i=1;i<=m;++i) { 26 scanf("%d%d",&l,&r); 27 if (n&1) { 28 if (l == n/2+1&&r == n/2+1) continue; 29 if (l == n/2+1) l++; 30 if (r == n/2+1) r--; 31 } 32 if (l<=n/2&&r>n/2) { 33 fit(r); 34 if (l>r) swap(l,r); 35 r--; 36 } else { 37 fit(l);fit(r); 38 if (l>r) swap(l,r); 39 } 40 flag[get_flag(l)]=flag[get_flag(r+1)]; 41 } 42 for (int i=1;i<=n/2+1;++i) sum[get_flag(i)] += dif[i]; 43 for (int i=1;i<=n/2+1;++i) if (fix(sum[i])!=0) { 44 return 0*puts("NO"); 45 } 46 puts("YES"); 47 return 0; 48 }
小结:一种重要解题方法就是对题目进行转化。