字符串 #2
By YYR
题目名称 基因的庇护 修仙 眼镜
输入文件名 protect.in hhh.in glasses.in
输出文件名 protect.out hhh.out glasses.out
每个测试点时限 1s 1s 2s
测试点数目 10 10 10
每个测试点分值 10 10 10
内存限制 128M 86M 512M
是否有部分分 否 否 否
题目类型 传统 传统 传统
1 基因的庇护(protect.c/cpp/pas)
1.1 题目描述
一只青蛙失去了荷叶的保护。
它十分迷茫,于是决定改变自己的基因,让自己成为一个受庇护的不会老去的物种。众所周知,青蛙的基因是一段有遗传效应DNA片段,我们认为这个片段仅由“A”,“T”,“C”,“G”组成,为了方便,我们只需考虑DNA的一条链。
这只青蛙十分有经验,它知道这条链长度为n,并且知道有庇护效应的序列有m种,它希望使得这条链上的每一个位置都受到庇护。注意有庇护效应的序列可以相互重叠,一个位置被庇护当且仅当它被至少一个有庇护效应的序列包含。
这只青蛙想要知道有多少种满足条件的链,对109+9取模。
1.2 输入格式
第一行两个整数n、m,表示链的长度和有庇护效应的序列数。
接下来m行,每行一个字符串表示一个有庇护效应的序列。注意同一个序列可能会重复出现。
1.3 输出格式
输出一行一个整数,表示青蛙想要的答案。
1.4 样例输入
6 2
CAT
TACT
1.5 样例输出
2
1.6 数据范围与约定
对于前20%的数据 n<=8 。
对于100%的数据1<=n<=1000,1<=m<=10 , 有庇护效应的序列长度不超过10。
题解
这道题以为记忆化搜索就能过, 后来才发现是自己愚蠢了. 建立AC自动机后, 相当于就是给你一张有向有环图, 问你走n步一共有多少种方案. 但是当然是由某些限制的.
考虑在AC自动机上面DP. dp[i][j][k]表示长度为i走到ac自动机上j节点还有末尾k个字符没有被庇护的方案数. 然后我们预处理一下以当前节点为结尾的后缀最多能庇护多长(即是庇护序列的最长后缀). 看一下能不能庇护k+1个, 能的话就转移到dp[i+1][tj][0], tj就是当前j的儿子. 不能的话就转移到dp[i+1][tj][k+1], 因为无法覆盖那么这次覆盖肯定是无效的, 所以多了一个未匹配的字符.
注意要用到补全AC自动机(Trie图). 因为可以从当前点跑到fail去匹配, 所以程序中我是for了所有儿子的, 不是有的才for, 因为我是补全了的, 没有的儿子会连向fail指向的有的地方. 当然如果不写补全AC自动机的话, 就要跳fail才行.
#include<stdio.h>
#include<queue>
#include<cstring>
#define deeper(a) memset(a, -1, sizeof(a))
using namespace std;
const int maxn = 1005;
const int mod = 1e9 + 9;
queue<int> q;
int num, n, m, tot, len;
int h[maxn], cnt[maxn], mean[maxn];
char ss[maxn];
struct acc{ int c[26], fail;}mm[maxn];
int f[maxn][maxn][11];
inline void build(){
int p = 0;
for(int i = 0; i < len; ++i){
int index = mean[(int)ss[i]];
if(!mm[p].c[index]) mm[p].c[index] = ++tot;
p = mm[p].c[index];
}
cnt[p] = len;
}
inline void bfs(){
for(int i = 0; i < 26; ++i){
int v = mm[0].c[i];
if(v){
mm[v].fail = 0; q.push(v);
}
}
while(!q.empty()){
int u = q.front(); q.pop();
for(int i = 0; i <= 3; ++i){
int v = mm[u].c[i];
if(!v) {mm[u].c[i] = mm[mm[u].fail].c[i]; continue;}
mm[v].fail = mm[mm[u].fail].c[i];
cnt[v] = max(cnt[v], cnt[mm[v].fail]);
q.push(v);
}
}
}
inline void add(int &a, int &b){
a += b;
if(a >= mod) a -= mod;
}
int dp(){
f[0][0][0] = 1;
for(int i = 0; i < n; ++i)
for(int j = 0; j <= tot; ++j)
for(int k = 0; k < 10; ++k)
if(f[i][j][k])
for(int p = 0; p <= 3; ++p){
int son = mm[j].c[p];
if(cnt[son] >= k + 1) add(f[i + 1][son][0], f[i][j][k]);
else add(f[i + 1][son][k + 1], f[i][j][k]);
}
int ans = 0;
for(int i = 0; i <= tot; ++i)
add(ans, f[n][i][0]);
return ans;
}
int main(){
freopen("protect.in", "r", stdin);
freopen("protect.out", "w", stdout);
mean['A'] = 0, mean['C'] = 1, mean['G'] = 2, mean['T'] = 3;
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++i){
scanf("%s", ss);
len = strlen(ss);
build();
}
bfs();
printf("%d\n", dp());
return 0;
}
2 修仙(hhh.c/cpp/pas)
2.1 题目描述
在获得庇护的同时,这只青蛙(或者称为别的什么物种)还获得了一股强大的力量。它的存在已经超出了自然世界的其他一切生物,于是它想要突破界限,掌握这股力量。
现在它得到了一本古老的书籍,并且将其翻译得到了一个长为n的由小写字母构成的字符串,现在,它想要找到一个最长的子串,使得这个子串既是字符串的前缀又是后缀,并且在字符串的中间出现过(即在非前后缀的位置出现过)。
由于剧情需要,我们保证存在这样一个满足条件的子串。
2.2 输入格式
仅一行一个字符串,表示翻译得到的字符串。
2.3 输出格式
输出一行一个整数,表示满足条件的子串的长度。
2.4 样例输入
ababacadbaba
2.5 样例输出
3
2.6 数据范围与约定
对于20%的数据,n<=5000
对于100%的数据,n<=10000000
题解
KMP的经典应用…不过好像为了卡hash只给了86M的空间. 不过班上某些写KMP的都还滥用空间爆掉… 下午都要被罚唱歌.
首先预处理nxt数组后看一下nxt[n]长度前缀的有没有出现过(判断2~n-1里有没有nxt等于nxt[n]的即可). 如果有的话输出nxt[n], 没有的话输出nxt[nxt[n]], 因为这一定是整个字符串中第二长的前后缀. 同时因为又是1~nxt[n]里的最大前后缀的长度, 所以在中间一定出现过(作为字符串中1~nxt[n]的后缀).
#include<stdio.h>
#include<cstring>
#include<algorithm>
#define clear(a) memset(a, 0, sizeof(a))
using namespace std;
const int maxn = 1e7 + 5;
int n, len, ans;
char s[maxn];
int nxt[maxn];
inline void getnxt(){
int j = 0;
for(register int i = 2; i <= n; ++i){
while(j && s[i] != s[j + 1]) j = nxt[j];
if(s[j + 1] == s[i]) ++j;
nxt[i] = j;
}
}
inline bool check(int x){
for(int i = 2; i < n; ++i)
if(nxt[i] == x) return true;
return false;
}
int main(){
freopen("hhh.in", "r", stdin);
freopen("hhh.out", "w", stdout);
scanf("%s", s + 1);
n = strlen(s + 1);
getnxt();
if(check(nxt[n])) ans = nxt[n];
else ans = nxt[nxt[n]];
printf("%d\n", ans);
}
/*
ababacadbaba
*/
3 眼镜(glasses.c/cpp/pas)
3.1 题目描述
这只小动物找到了书中的力量,它几乎就要成功了,依据书中内容,它还缺一副眼镜。
于是它找到了一个01串,想要从中找到制造眼镜的材料。它希望找到这个01串的最长的子序列串(即不要求连续),这个子序列满足01交间的性质(01010…或10101…)。
但是在寻找之前,它想测试一下目前拥有的力量,于是它选择了一段连续的区间,将这个区间中的0变成1、1变成0,再在其中寻找最长01交间子序列串。
当然,它希望合适地运用仅一次自己的力量(但一定要使用),使得这个子序列串变得尽可能长。
3.2 输入格式
仅一行一个长为n的01字符串。
3.3 输出格式
输出一行一个整数表示答案。
3.4 样例输入
10000011
3.5 样例输出
5
3.6 样例解释
将其变为10011011,这个串中的满足条件的子序列串长度即为5,可以证明这是最长的长度。
3.7 数据范围与约定
对于10%的数据,保证 n<=500。
对于30%的数据,保证 n<=5000。
对于100%的数据,保证n<=500000。
题解
贪心即可.
既然是数01块数,那么思考一下题目性质,当我们反转一个区间时,这个区间内部的01块数是不会改变的,那么只有这个区间的两端会造成影响。
推导一下就可以发现,我们令有k组相邻的并且数字相同的位置。
那么当k=0时,字符串已经是完全的01相间,反转整个字符串保持答案不改变即可;当k=1时,如10100101,那么找到这组的后一个字符的位置,将这个位置及以后全部反转,得到10101010,显然答案一定会增加1;当k=2时,如101011010010,那么将第一组的第二个字符到第二组的第一个字符间全部反转,得到101010101010,显然答案一定会增加2;当k>=3时,由于只能影响反转区间的两端,所以最多只能使得答案增加2,而通过k=2的构造一定能使答案增加2。
实现就十分简单了,再令有t组相邻的并且数字不同的位置,答案就是t+min(2,k)。
另外这道题也可以DP,同样不难。
#include<stdio.h>
#include<cstring>
const int maxn = 1e6 + 5;
bool flag;
int n, cnt, all, num;
char s[maxn], last;
int main(){
freopen("glasses.in", "r", stdin);
freopen("glasses.out", "w", stdout);
scanf("%s", s + 1);
n = strlen(s + 1);
last = '#';
for(int i = 1; i <= n; ++i){
if(s[i] != last){
++all;
if(num >= 2) ++cnt;
if(num >= 3) flag = true;
num = 1;
}
else num++;
last = s[i];
}
if(cnt >= 2 || flag) printf("%d\n", all + 2);
else if(cnt == 1) printf("%d\n", all + 1);
else printf("%d\n", all);
}