P1039 翻转字符 官方题解
Gioush OJ · P1039 翻转字符
翻转字符
【题意简述】
给出字符串 ,记其翻转串为 。每次可以交换 中相邻的两个字符,求把 变为 的最少交换次数。
【Hint】
【提示】
相同字符按出现顺序进行对应不会更劣。建立 中每个字符对应到 中位置的排列后,答案就是这个排列的逆序对数量。
【数据点 1∼21\sim 21∼2】
用二进制集合 表示已经从 中取出的位置。令 ,下一步枚举 且 ,把 移到未取出字符的最前方。代价是它左侧尚未取出的字符数量。
状态数为 。时间复杂度为 ,空间复杂度为 。
【数据点 3∼53\sim 53∼5】
直接维护当前字符串。从左到右处理目标位置 ,在当前位置之后找到第一个等于 的字符,再通过相邻交换把它移动到位置 。时间复杂度为 。
【正解】
先确定重复字符的对应方式。固定字符 ,设它在 中依次出现于
在 中依次出现于
可以令 对应 。若 却分别对应 ,交换两个相同字符的目标位置不会改变最终字符串,并且会消去这一次交叉。对于任意第三个目标位置,交换也不会增加逆序关系。因此反复消去交叉后,可以得到按出现顺序对应的最优方案。
按照 中字符的顺序,依次取出该字符在 中尚未使用的最早位置,得到一个排列 。
若 且 ,这两个字符在初始串与目标串中的相对顺序不同,二者至少交换一次。一次相邻交换只改变一对字符的相对顺序,所以操作次数至少为排列的逆序对数量。
另一方面,按照冒泡排序逐一消去逆序对,每次交换都使逆序对数量减少 ,最终恰好得到目标顺序。因此最少交换次数就是排列的逆序对数量。
不需要显式构造 。先把 中每种字符的出现位置从左到右放入队列,再倒序扫描 ,每次弹出对应队列的队首位置,便可得到排列。随后使用线段树维护已经出现的位置,查询此前加入且大于当前位置的元素数量。
【复杂度分析】
每个位置执行一次查询与一次修改。时间复杂度为 ,空间复杂度为 。答案最大为 ,需要使用 long long。
【参考代码】
#include <queue>#include <cstdio>#define MAXN 200010using namespace std; int c,n,Line[MAXN],Tree[MAXN<<2];char Str[MAXN];queue <int> Pos[26]; void Modify(int Now,int l,int r,int x) { if (l==r) { Tree[Now]=1; return; } int Mid=(l+r)>>1; if (x<=Mid) Modify(Now<<1,l,Mid,x); else Modify(Now<<1|1,Mid+1,r,x); Tree[Now]=Tree[Now<<1]+Tree[Now<<1|1];} int Query(int Now,int l,int r,int x,int y) { if (x>y||l>y||r<x) return 0; if (l>=x&&r<=y) return Tree[Now]; int Mid=(l+r)>>1; return Query(Now<<1,l,Mid,x,y)+Query(Now<<1|1,Mid+1,r,x,y);} int main() {#ifndef ONLINE_JUDGE freopen("flip.in","r",stdin); freopen("flip.out","w",stdout);#endif scanf("%d",&c); scanf("%d%s",&n,Str+1); for (int i=1;i<=n;i++) Pos[Str[i]-'a'].push(i); for (int i=n;i>=1;i--) { int x=Str[i]-'a'; Line[n-i+1]=Pos[x].front(); Pos[x].pop(); } long long Ans=0; for (int i=1;i<=n;i++) { Ans+=Query(1,1,n,Line[i]+1,n); Modify(1,1,n,Line[i]); } printf("%lld\n",Ans); return 0;}