Str 真题解(置换)
题面
对于字符串 \(s\) 定义一个变换 \(f(s)\) 表示, \(\forall 1\le k\le \lfloor|s|/2\rfloor\) ,将 \(s\) 中从后往前第 \(k\) 个字符插入从前往后第 \(k\) 个字符和第 \(k+1\) 个字符之间后得到的字符串,例如 \(\texttt{abcdef}\) 变换后得到 \(\texttt{afbecd}\) .
现在给出用 \(f(s)\) 变换过 \(k\) 次以后的字符串(即执行 \(k\) 次 \(s:=f(s)\)),求变换之前的字符串 .
(别骂我,这是原题面)
\(3\le |s|\le 10^3, 1\le k\le 10^9\) .
置换
这里没有群论
这里没有群论 .
这里的置换是狭义的,正经置换看 OI-Wiki(内含 Burnside,慎入)
置换
一个置换 \(p\) 定义为一个排列,置换相当于一个运算,将原来在位置 \(i\) 的东西变到位置 \(p_i\) .
置换的乘法(复合)
你对一个东西施加置换 \(p\) 然后施加置换 \(q\),就相当于施加 \(p\circ q\) .
显然新的置换 \(h=p\circ q\) 由下式直接表示:
置换乘法的单位元
恒等置换 \(id\) 定义为 \(id_i=i\) .
显然任何置换 \(p\) 满足 \(p\circ id = id \circ p\),于是 \(id\) 就是 \(\circ\) 的单位元 .
置换乘法的结合律
结合律:
为啥?
这里我们用函数表示下标,因为下标实在太多了 .
Q.E.D. 是不是很显然 .
置换快速幂
有单位元,有结合律,显然可以 \(\log\) 快速幂吧 .
置换求乘法逆
显然求逆就是把置换逆过来了(类似反函数?)(从施加的角度看,真的很显然)
原来是 \(i\to p_i\),现在就是 \(p_i\to i\) .
直接模拟算就完了 .
真题解
题目要求的 \(f\) 可以看做一个置换 \(p\) .
于是 \((p^{k})^{-1}\) 就是我们要对字符串 \(s\) 施加的置换 .
直接算出来,时间复杂度 \(O(|s|\log k)\) .
一种可能的代码实现
因为求逆可以先求也可以后求甚至可以直接拿眼看出来,所以可能的代码实现有很多 .
这里是先快速幂再求逆,应该是好理解的 .
// 增加了注释
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <vector>
#include <queue>
#include <cmath>
#include <map>
#include <set>
#include <bitset>
#include <cassert>
using namespace std;
const int N = 1555;
int k;
string s;
struct perm // 置换
{
perm(int x=1){n=x; for (int i=1; i<=x; i++) a[i] = i;} // 初始化为恒等置换
int operator [](const unsigned& id)const{return a[id];}
int& operator [](const unsigned& id){return a[id];} // 元素
perm operator * (const perm& rhs)const // 乘法
{
assert(n == rhs.n);
perm x(n);
for (int i=1; i<=n; i++) x[i] = rhs[a[i]];
return x;
}
perm& operator *= (const perm& rhs){return *this = *this * rhs;}
perm inv() // 求逆
{
perm ans(n);
for (int i=1; i<=n; i++) ans[a[i]] = i;
return ans;
}
inline void prt() // debug
{
for (int i=1; i<=n; i++) printf("%d ", a[i]);
puts("");
}
inline size_t size()const{return n;}
private:
int n, a[N];
};
perm qpow(perm a, int n) // 快速幂
{
perm ans(a.size());
while (n)
{
if (n&1) ans *= a;
a *= a; n >>= 1;
} return ans;
}
perm create(int n) // 生成题目说的变换 f
{
perm ans(n);
int ptr1 = 1, ptr2 = n, cc = 0;
while (ptr1 <= ptr2){ans[++cc]=ptr1; if (cc<n) ans[++cc]=ptr2; ++ptr1; --ptr2;}
return ans;
}
int main()
{
scanf("%d", &k);
cin >> s; int l = s.length(); s = "$" + s;
perm ans = qpow(create(l), k).inv();
for (int i=1; i<=l; i++) putchar(s[ans[i]]); // 对 s 施加置换
puts("");
return 0;
}
关于循环节做法
看起来这个东西有循环节?打了一发直接过了,出题人可真 sb
实际上我们可以证明这个东西有循环节且循环节是不大于 \(|s|\) 的 .
从置换的角度考虑,如果 \(i\) 能到 \(p_i\) 就连一条 \(i\to p_i\) 的有向边 .
\(n\) 个点 \(n\) 条边显然可以构成一个内向基环树森林,运算相当于在图上走一次,一直走肯定能走到环上 .
这说明任何置换 \(p\) 的方幂都是有循环节的 .
然而这题里的 \(p\) 更加特殊:
咕咕咕
以下是博客签名,正文无关
本文来自博客园,作者:Jijidawang,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/15889394.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ