题目大意:
就是现在初始的时候没有任何DNA链, 现在又无数个A, G, C, T这些材料无数个, 每次可以在已经有的链上选择2中操作:
1. 在链的的左边或者右边加上A, G, C, T的其中一个
2. 将现有的链镜像相连如AGC镜像可以变成AGCCGA或CGAAGC
给出一个长度不超过10^5的目标串, 问最少需要几次操作完成这样一条链, 其实的链为空
大致思路:
这个题挺难的, 想了好久才想明白
首先注意到对于目标串, 最优的构造策略一定是构造出那个串当中的回文字串部分然后再将其他字符添上
那么我们便演变成需要解决对于一个回文字串最少需要几步构造出来的问题
首先我们将这个字符串插入到Palindromic Tree当中, 这样我们可以找到所有的回文串, 因为镜像操作只能加速生成偶数长度的回文串, 我们只需要考虑偶数长度的回文串如何被快速构造即可
在Palindromic Tree中用dp[u]表示结点u所表示的回文串需要几步完成
对于u的儿子结点v, v是由u的左右加上一个相同字符组成
下面我们从两种不同的镜像方式来证明 dp[v] = min(dp[u] + 1, len[v]/2 - len[ halflink[v] ] + dp[ halflink[v]] + 1)这个转移方程的正确性 其中 halflink[v] 这个结点是指长度不超过v的一半的最长的v的后缀回文串也就是 sufflink(相当于fail指针)的另一种形式, 在sufflink的要求上加上了长度的要求
对于每一种长度为偶数的回文串v, 这里为了方便我设u -> v两边加上的是'a'
v可以是u在镜像之前将‘a'先添到一侧然后沿着另外一侧镜像得到的, 那么得到v的步数是dp[u] + 1, 这个根据dp的性质, 所有在一侧添加字幕然后沿另外一侧翻转的策略中, dp[u] + 1显然是这种镜像中最优的
那么还有一种镜像得到v的方式是, 在一侧添加一定字符之后, 沿着相同的这一侧镜像
例如A右边加上TC变成ATC然后沿着右边镜像, 亦或者都是左边, 这种镜像是另外一种的, 上面那一部分dp没有考虑到
这样的情形下对于串S = 'a' + X + X + 'a', 由于对成性我们假设是在左边添加字符之后向左边镜像得到的S, 那么转换到S的那个串T的长度不能超过S的一半, 也就是之前定义halflink[v]指向的串长度不超过v的一半的原因, 当然同样的这个串T, 如果T是偶数长度的串, 也就成为了子问题dp[t]已经解出, 如果是奇数长度的串, 可能这个技术长度的串可以在其字符个数的步骤内完成, 但是都是没有必要考虑的, 奇数长度的T都不用考虑, 为什么呢?
假设halflink[v]指向的是T这样一个长度为奇数, 如果这个串中没有偶数回文子串的话, 就只能逐个字母累加出来, 那么相对于之前的dp[u] + 1的策略来说没有任何优势
如果这个奇数回文串包含一部分偶数回文作为字串(可以不用逐个累加得到), 那么就会出现下图的情况:
所以当T为奇数的时候直接不用考虑了, 于是只需要记录偶数长度回文串的解即可, bfs遍历回文树求解对于每一个可能作为最终串形成前最后镜像得到的偶数长度回文串做一下比较答案即可
代码如下:
Result : Accepted Memory : 4044 KB Time : 1170 ms
/*
* Author: Gatevin
* Created Time: 2015/4/1 13:11:43
* File Name: Rin_Tohsaka.cpp
*/
#include<iostream>
#include<sstream>
#include<fstream>
#include<vector>
#include<list>
#include<deque>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<bitset>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cctype>
#include<cmath>
#include<ctime>
#include<iomanip>
using namespace std;
const double eps(1e-8);
typedef long long lint;
#define maxn 100010
#define CHAR_SIZE 4
struct Palindromic_Tree
{
int next[maxn][CHAR_SIZE];//两边加上对应的char之后到达的结点
int len[maxn];//当前结点代表的回文串长度
int sufflink[maxn];//相当于fail指针 指向这个位置失配之后应该找的largest suffix-palindrome位置
int cnt[maxn];//存储的相当于是以这个结点的回文结尾作为结尾的所有回文串的数量
int times[maxn];//记录每个node代表的回文串本身出现的次数
int L, N, last;//总点数, 已经插入的字符数, 上次插入字符后largest suffix-palindrome的位置
char s[maxn];//插入的串
int halflink[maxn];//不用在意....
void newnode()
{
L++;
for(int i = 0; i < CHAR_SIZE; i++)
next[L][i] = -1;
len[L] = sufflink[L] = cnt[L] = times[L] = 0;
return;
}
void init()//node 1 -- root with len -1, node 2 -- root with len 0
{
L = 0, last = 2, N = 0;
newnode(), newnode();
len[1] = -1, sufflink[1] = 1;//1是奇数长度回文串的根, 奇数根结点sufflink指向自己
len[2] = 0, sufflink[2] = 1;//2是偶数长度回文串的根, 偶数根结点sufflink指向奇数根
halflink[2] = 2;
return;
}
int get_sufflink(int x)
{
while(s[N - 1 - len[x]] != s[N]) x = sufflink[x];
return x;
}
int enCode(char c)
{
switch(c)
{
case 'A': return 0;
case 'G': return 1;
case 'C': return 2;
case 'T': return 3;
}
return -1;
}
bool addLetter(int pos)
{
bool addnew = false;
int alp = enCode(s[pos]);;//插入新的字符
N = pos;
int cur = get_sufflink(last);//找到xAx的A
if(next[cur][alp] == -1)//这个点不存在, 需要新增
{
newnode();
len[L] = len[cur] + 2;
next[cur][alp] = L;
if(len[L] == 1)//单个字符的sufflink连接到偶数根节点, 特判..
sufflink[L] = 2;//不特判会连到sufflink[1] = 1上
else
sufflink[L] = next[get_sufflink(sufflink[cur])][alp];//否则可以向上找
cnt[L] = cnt[sufflink[L]] + 1;
addnew = true;
//接下来是寻找halflink
if(len[L] == 1)
halflink[L] = 2;
else if(len[L] == 2)
halflink[L] = sufflink[L];
else {
//int res = sufflink[cur];如果从sufflink开始的话会TLE
int res = halflink[cur];
//利用上一个的halflink走, 这样复杂度降低, 其实就和get_sufflink的时候一直沿着sufflink走一个道理
while(s[N - 1 - len[res]] != s[N] || 2*(len[res] + 2) > len[L]) res = sufflink[res];
halflink[L] = next[res][alp];
}
}
last = next[cur][alp];//更新last位置
times[last]++;//last这个位置又出现了一次
return addnew;//是否出现了新的回文串
}
void count()
{
for(int i = L; i > 0; i--) times[sufflink[i]] += times[i];
return;
}
int dp[maxn];
void solve()
{
init();
scanf("%s", s);
int length = strlen(s);
for(int i = 0; i < length; i++)
addLetter(i);
dp[1] = 0, dp[2] = 1;//初始化偶数起点步骤是1, 奇数起点是0
queue <int> Q;
Q.push(2);
int ans = length;
while(!Q.empty())
{
int now = Q.front();
Q.pop();
for(int i = 0; i < CHAR_SIZE; i++)
if(next[now][i] != -1)
{
int nex = next[now][i];
dp[nex] = dp[now] + 1;
if((len[halflink[nex]] & 1) == 0)//halflink指向的是奇数的不用理
dp[nex] = min(dp[nex], len[nex]/2 - len[halflink[nex]] + dp[halflink[nex]] + 1);
ans = min(ans, length - len[nex] + dp[nex]);
Q.push(nex);
}
}
printf("%d\n", ans);
return;
}
};
Palindromic_Tree pal;
int main()
{
int T;
scanf("%d", &T);
while(T--)
pal.solve();
return 0;
}