暑期集训字符串与搜索
字符串与搜索
C.归并排序
题意:对两个无序且长度任意的序列a和b进行归并,同时要求结果的字典序最小
分析:其实就是每次取剩余序列中字典序更小的那一侧的点。
- 当$a_i\neq b_j$时,取小的那个;
- 当$a_i=b_j$时,这两个点分别向后找,把与这两个点值一样的都去掉(一直找到值不一样的两个点),然后输出较小的那个即可。
然而如果直接这样去一个一个找的话,在遇到类似$aaaa…b$这样的序列时,每次都要扫一遍序列,复杂度很高,需要优化。
优化:对于连续相同的一系列数,我们让它们都指向这一系列数的末尾,这样每次向后查询与当前数字不同的数字时可以一步到位,直接跳到该系列数的最后一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int n, m;
int a[N], b[N], c[2 * N];
int a_dex[N], b_dex[N];
int cnt, ai = 1, bi = 1, aai = 1, bbi = 1;
void solve() {
memset(a, 63, sizeof(a)) ,memset(b, 63, sizeof(b));
memset(a_dex, 63, sizeof(a_dex)), memset(b_dex, 63, sizeof(b_dex));
si(n); for (int i = 1; i <= n; i++)si(a[i]);
si(m); for (int i = 1; i <= m; i++)si(b[i]);
for (int i = n; i >= 1; i--) a_dex[i] = a[i] == a[i + 1] ? a_dex[i + 1] : i;
for (int i = m; i >= 1; i--) b_dex[i] = b[i] == b[i + 1] ? b_dex[i + 1] : i;
while (cnt < n + m) {
if (a[ai] > b[bi])
c[++cnt] = b[bi++];
else if (a[ai] < b[bi])
c[++cnt] = a[ai++];
else {
aai = ai, bbi = bi;
if (ai == n)
c[++cnt] = b[bi++];
else if (bi == m)
c[++cnt] = a[ai++];
else {
while (1) {
aai = a_dex[aai] + 1, bbi = b_dex[bbi] + 1;
if ((aai - ai) != (bbi - bi)) {
c[++cnt] = (aai - ai) < (bbi - bi) ? a[ai++] : b[bi++];
break;
}
else if (a[aai] != b[bbi]) {
c[++cnt] = a[aai] < b[bbi] ? a[ai++] : b[bi++];
break;
}
if (aai > n) { c[++cnt] = b[bi++]; break;}
if (bbi > m) {c[++cnt] = a[ai++]; break;}
}
}
}
}
for (int i = 1; i <= n + m; i++)printf("%d ", c[i]);
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
G. 进化
题意:起始有得分$A_0$,每次可以选一个数字$A_i$使得分加上$A_i\,mod\,7$,这个$A_i$时通过上次选的数字通过复杂公式得到两个数字,求几轮后可以让得分大于等于300
分析:每次得分最多加6,也就是说至少要扫50层,直接$dfs$会$TLE$,于是考虑使用可行性剪枝+迭代加深。
可行性剪枝:这个剪枝相当重要。首先我们有一个最大搜索深度,在当前深度,如果剩余的搜索里每次都加6还达不到300的话就退出,搜到之后更新一下最大搜索深度即可。
迭代加深:每次设定一个搜索深度上限,搜索深度大于设定值(还没搜到)时就退出,然后增加搜索深度上限重新搜索。这种做法的有点是:有类似$bfs$的处理效果,但又有$dfs$的空间复杂度。
然而我最初写的迭代加深+可行性剪枝会在39到41的$test$点$TLE$,后来更改为每次迭代加深+2,找到后往回扫一次取最小值。这样每次跳两步的迭代加深比每次跳一步的快一点点就过了。就嗯卡过去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <bits/stdc++.h>
template<typename T>
inline void read(T& x) { x = 0; char c = getchar(); while (!isdigit(c))c = getchar(); while (isdigit(c)) { x = x * 10 + c - '0'; c = getchar(); } }
#define si(a) read(a)
#define sii(a,b) read(a),read(b)
#define siii(a,b,c) read(a),read(b),read(c)
#define fl float
#define ll long long int
#define ull unsigned long long int
using namespace std;
int gcd(int a, int b) { return a == 0 ? b : gcd(b % a, a); }
//const ll MOD = 924844033;
const int N = 2e6 + 10;
int n, m, S;
int flag = 1, now, ans = 114514;
ll seed, MOD = 1LL << 32;
ll A1(ll A0) { return (213346089LL * A0 + 870413LL) % MOD; }
ll A2(ll A0) { return (166042049LL * A0 + 598777LL) % MOD; }
void dfs(int u, int lim, int sum, ll food) {
if (!flag || u>lim) return;
ll a1 = A1(food), a2 = A2(food);
if (sum >= S) {
flag = 0, ans = min(ans,u);
return;
}
if (sum + 6 * (lim - u) < S)
return;
dfs(u + 1, lim, sum +a1 % 7, a1);
dfs(u + 1, lim, sum + a2 % 7, a2);
}
void solve() {
sii(seed, S);
now = -1;
while (flag) {
now += 2;
dfs(1, now, seed % 7, seed);
}
flag = 1;
dfs(1, now-1, seed % 7, seed);
printf("%d", ans);
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
/*
*/
H. 回文串
题意:对于一个串 s,定义 s 的某个子串的权值为其在原串的出现次数乘以该子串的长度。给定串 s, 求其所有回文子串的最大权值。
分析: 回文自动机,同时记录回文串的出现次数
回文自动机/回文树
结构:像AC自动机一样,我们有一个fail指针用来跳字符树。有两个字符树,0表示回文串长度为偶数的树,1表示回文串长度为奇数的树。每个节点虽然只有一个字符,其表示的其实是一个回文串,比如:0→a→b,a节点表示回文串aa,b表示回文串abba。对于每个节点,它的fail指针指向这个节点表示的回文串的后缀中最长的回文串。特殊的有:0节点fail指针指向1,其他非1、0节点若后缀无回文串指向0,1节点的fail指针不用考虑。
操作:每次读入一个字符,从上次的后缀回文(用last存一下)跳fail指针直到能构成回文,更新回文树,再对这个节点跳fail指针获得它的fail指针。
跳fail指针:
1
int getfail(int x, int now) { while (s[now - len[x] - 1] != s[now])x = fail[x]; return x; }//x是当前fail指针指向的,now是当前处理的节点;返回最长后缀回文
搭建当前节点的fail指针:
1
fail[q] = tr[getfail(fail[p], i)][s[i]];//tr[][]为回文树
同时,对于该题我们还需要统计一下回文串的出现次数,这里可以直接在每次处理的回文串对应处cnt++,但这样有个小问题,长的回文串可能会包含小的回文串,而此时小的回文串并没有考虑到长的回文串的计数。所以在最后count的时候逆序累加,让每个父节点(小回文串)都能加上所有后代(大回文串),同时更新ans。
1
2
3
4
for (int i = tot; i > 0; i--) {//tot是回文树的节点数
cnt[fail[i]] += cnt[i];
ans = max(ans, cnt[i] * len[i]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <bits/stdc++.h>
template<typename T>
inline void read(T& x) { x = 0; char c = getchar(); while (!isdigit(c))c = getchar(); while (isdigit(c)) { x = x * 10 + c - '0'; c = getchar(); } }
#define si(a) read(a)
#define sii(a,b) read(a),read(b)
#define siii(a,b,c) read(a),read(b),read(c)
#define fl float
#define ll long long int
#define ull unsigned long long int
using namespace std;
int gcd(int a, int b) { return a == 0 ? b : gcd(b % a, a); }
//const ll MOD = 924844033;
const int N = 2e6 + 10;
int n, m;
ll ans = 0;
char s_[N];
int p, q, pp = 0;
int s[N];
int cnt[N], len[N], tot = 1, tr[N][26], fail[N];
int getfail(int x, int now) { while (s[now - len[x] - 1] != s[now])x = fail[x]; return x; }//返回最长后缀回文
void start() {//初始化
for (int i = 1; s_[i]; i++)s[i] = s_[i] - 'a';
s[0] = -1;
len[0] = 0, len[1] = -1;
fail[0] = 1;
}
void solve() {
scanf("%s", s_+1);
start();
int s_len = strlen(s_ + 1);
for (int i = 1; i<=s_len; i++) {
p = getfail(pp, i);//从上一次的后缀回文开始找能回文的地方
if (!tr[p][s[i]]) {
len[++tot] = len[p] + 2, q = tot;
fail[q] = tr[getfail(fail[p], i)][s[i]];//搭fail
tr[p][s[i]] = q;
}
pp = tr[p][s[i]];
cnt[pp]++;
}
for (int i = tot; i > 0; i--) {
cnt[fail[i]] += cnt[i];//逆序累加,所以每个父节点都能加上所有的后代
ans = max(ans, (ll)(cnt[i]) * (ll)(len[i]));
}
printf("%lld\n", ans);
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
/*
*/
Q. 接头暗号
题意: 给定一个文本t和一个字符串s,求t中s的出现次数。
KMP
首先要了解前缀函数:给定一个长度为$n$的字符串 ,其前缀函数被定义为一个长度为$n$的数组$\pi$。其中$\pi[i]$是子串$s[0…i]$最长的相等的真前缀与真后缀的长度。
那么,计算π[i]的朴素算法显然就是不停的扫,时间复杂度为$O(n^3)$。可以发现,每次读入一个字符时,如果$\pi[i+1]=\pi[i]+1$,那么就有$s[i+1]=s[\pi[i]]$(数组从0开始),也就是说,会出现下面的情况:
图来自OI Wiki
那如果$s[i+1]\neq s[\pi[i]]$如何去找呢?我们还会发现,对于子串$s[0…i]$,首先有$s[0…\pi[i]]=s[i-\pi[i]+1…i]$,然后就会发现,这两个小子串完全一致就会导致前面那个小子串$s[0…\pi[i]]$的前缀性质会保留到后一个小子串$s[i-\pi[i]+1..i]$,也就是$s[0…\pi[i]]$又会有一个前缀和$s[i-\pi[i]+1..i]$的后缀相等,而这个前缀/后缀的长度显然就是$\pi[\pi[i]]$,就是下面图所示:
图还是来自OI Wiki
所以在失配的时候就可以不停的跳π[i]来找相等的前缀,时间复杂度为$O(n)$
回到$KMP$,将字符串$s$和文本$t$拼接起来,中间用一个两者都没有的字符(如’#’)隔开,然后跑前缀函数即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <bits/stdc++.h>
template<typename T>
inline void read(T& x) { x = 0; char c = getchar(); while (!isdigit(c))c = getchar(); while (isdigit(c)) { x = x * 10 + c - '0'; c = getchar(); } }
#define si(a) read(a)
#define sii(a,b) read(a),read(b)
#define siii(a,b,c) read(a),read(b),read(c)
#define fl float
#define ll long long int
#define ull unsigned long long int
using namespace std;
int gcd(int a, int b) { return a == 0 ? b : gcd(b % a, a); }
//const ll MOD = 924844033;
const int N = 5e5 + 10;
int n, m;
char s[5000000];
char t[5000010];
int ans;
int pi[5000010];
void solve() {
scanf("%s", s);
scanf("%s", t);
int cao = strlen(t);
t[cao] = '#';
strcat(t, s);
int len = strlen(t);
for (int x = 1; x < len; x++) {
int y = pi[x - 1];
while (y > 0 && t[x] != t[y])
y = pi[y - 1];
if (t[x] == t[y])
y++;
pi[x] = y;
if (pi[x] == cao)
ans++;
}
printf("%d", ans);
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
/*
3
what
is
mind
thisisnotmind
*/
R. 国际象棋
题意:5×5的棋盘,有12个黑子、12个白子和一个空格,每个子的移动方式为国际象棋中马的走法,要求最后棋局长这样:
1
2
3
4
5
11111
01111
00*11
00001
00000
1是黑子,0是白子,*是空格。求最小步数(步数大于15输出-1)
迭代加深+可行性剪枝
- 由于最大步数就是15,直接设搜索深度为15即可。
- 移动一次会改变两个子的位置,移动两次最多改变三个子的位置,移动n次最多改变n+1个子的位置,我们需要记录并计算当前棋局与终局棋子的差异个数,如果差异次数大于剩余次数+1,那就是不可能在剩余步数里下完棋局,就可以直接退出了。
- 每次枚举空格周围的8个格子而不是枚举每个棋子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
const int N = 2e6 + 10;
int n, m;
int ans;
int next_x[9] = { 0,-2,-1,1,2,2,1,-1,-2 };
int next_y[9] = { 0,1,2,2,1,-1,-2,-2,-1 };
char chess[6][6];
char chess_end[6][6] = {
{'1','1','4','5','1','4'},
{'w','1','1','1','1','1'},
{'c','0','1','1','1','1'},
{'w','0','0','*','1','1'},
{'c','0','0','0','0','1'},
{'A','0','0','0','0','0'}
};
bool check_xy(int a, int b, int th) {
if ((a + next_x[th] >= 1) && (b + next_y[th] >= 1) && (a + next_x[th] <= 5) && (b + next_y[th] <= 5))
return true;
return false;
}
bool ok_() {
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
if (chess_end[i][j] != chess[i][j])
return false;
return true;
}
int diff() {
int cnt = 0;
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
if (chess_end[i][j] != chess[i][j])
cnt++;
return cnt;
}
void dfs(int u, int x,int y, int now) {
if (u > 15 || u>=ans)
return;
if (!now) {
ans = min(ans, u);
return;
}
if (now > 16 - u) return;
for (int i = 1; i <= 8; i++)
if (check_xy(x, y, i)) {
int tmp = 0;
int x2 = x + next_x[i], y2 = y + next_y[i];
if (chess[x][y] == chess_end[x][y]) tmp++;
if (chess[x2][y2] == chess_end[x2][y2]) tmp++;
if (chess[x][y] == chess_end[x2][y2])tmp--;
if (chess[x2][y2] == chess_end[x][y]) tmp--;
swap(chess[x][y], chess[x + next_x[i]][y + next_y[i]]);
dfs(u + 1, x + next_x[i], y + next_y[i],now+tmp);
swap(chess[x][y], chess[x + next_x[i]][y + next_y[i]]);
}
}
void solve() {
ans = 114514;
for (int i = 1; i <= 5; i++)
scanf("%s", chess[i]+1);
getchar();
int tmp = diff();
for (int i = 1; i <= 5; i++)
for (int j = 1; j <= 5; j++)
if (chess[i][j] == '*') {
dfs(0, i, j, tmp);
if (ans >15) printf("-1\n");
else printf("%d\n", ans);
return;
}
}
int main() {
int t;
si(t);
while (t--)
solve();
return 0;
}
/*
2
1*111
01111
00111
00001
00000
01011
110*1
01110
01010
00100
*/
S. 井字棋
题意:普普通通井字棋,规定井字棋终局的得分为相同子连成一条线的个数,有$T$个井字棋的棋局,指定下一步是谁下的,玩家$a$想让终局分数最大,玩家$b$想让其最小,$a$下’$O$’,$b$下’$X$’,两人每一步都是最优的,求$T$个井字棋棋局的最终得分
究极打表
考虑到最终棋局的状态只有$2×3^9=39366$种,这个数字相当之小,如果我们能把每个残局都直接指向其终局,那么$T$个棋局直接查询就能得到答案。
棋局的储存
采用一个18位二进制来保存一个棋局,每两位表示一个格子的状态:00为空,01为’$O$’,10为’$X$’
记忆化搜索
- $dfs$遍历棋盘9个格子的3种状态(’X’,’O’,空),如果当前棋局没有遍历过就继续$dfs$(记忆化保证不重复搜索)
- 设$ans_a[i]$为在棋局i下a先手的最终得分,$ans_b[i]$同理;设$i_X$为在棋局$i$上(某一点)下’$X$’的最终得分,$i_O$同理,那么$ans_{a[i]}$的取值即为$max\,{ans_a[i],ans_b[i_X]}$,即取下完’$X$’后的棋局里$b$的得分,因为$a$下子后是$b$走,$ans_b[i]$的取值同理为$min\,{ans_b[i],ans_a[i_O]}$。为什么能直接取$ans_b[i_X]$和$ans_a[i_O]$,因为$max$和$min$的操作是在当层$dfs$之后的,也就是更新操作是从终局向空棋盘进行更新的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
int n, m;
/*
01-->gp 10-->na
0 1 2
3 4 5
6 7 8
*/
char chess[9];
ll gp_nxt[9], na_nxt[9];
map<ll, int>ans;
map<ll, int>vis;
map<ll, int>ans_gp;
map<ll, int>gp_vis;
map<ll, int>ans_na;
map<ll, int>na_vis;
bool if_exist(ll x, int dex) { return x & (3 << (2 * dex)); }
int score() {//计算分数
int sum = 0;
if (chess[0] == 'O' && chess[1] == 'O' && chess[2] == 'O') sum++;
if (chess[3] == 'O' && chess[4] == 'O' && chess[5] == 'O') sum++;
if (chess[6] == 'O' && chess[7] == 'O' && chess[8] == 'O') sum++;
if (chess[0] == 'O' && chess[3] == 'O' && chess[6] == 'O') sum++;
if (chess[1] == 'O' && chess[4] == 'O' && chess[7] == 'O') sum++;
if (chess[2] == 'O' && chess[5] == 'O' && chess[8] == 'O') sum++;
if (chess[0] == 'O' && chess[4] == 'O' && chess[8] == 'O') sum++;
if (chess[2] == 'O' && chess[4] == 'O' && chess[6] == 'O') sum++;
if (chess[0] == 'X' && chess[1] == 'X' && chess[2] == 'X') sum--;
if (chess[3] == 'X' && chess[4] == 'X' && chess[5] == 'X') sum--;
if (chess[6] == 'X' && chess[7] == 'X' && chess[8] == 'X') sum--;
if (chess[0] == 'X' && chess[3] == 'X' && chess[6] == 'X') sum--;
if (chess[1] == 'X' && chess[4] == 'X' && chess[7] == 'X') sum--;
if (chess[2] == 'X' && chess[5] == 'X' && chess[8] == 'X') sum--;
if (chess[0] == 'X' && chess[4] == 'X' && chess[8] == 'X') sum--;
if (chess[2] == 'X' && chess[4] == 'X' && chess[6] == 'X') sum--;
return sum;
}
void trs(ll x) {//转化为棋盘
for (int i = 0; i < 9; i++) {
int tmp = x & 3;
if (tmp == 1) chess[i] = 'O';
else if (tmp == 2) chess[i] = 'X';
else if (tmp == 0) chess[i] = '.';
tmp >>= 2;
}
}
ll trs_x() {//转化为数字(状态)
ll num = 0LL, tmp;
for (int i = 8; i >= 0; i--) {
if (chess[i] == 'O') tmp = 1LL;
else if (chess[i] == 'X') tmp = 2LL;
else if (chess[i] == '.') tmp = 0LL;
num = (num << 2) | tmp;
}
return num;
}
void dfs(ll now) {
ll tmp, tmp2;
int flag = 1;
for (int p = 0; p < 9; p++) {
if (if_exist(now, p))
continue;//p位置是否有数字
flag = 0;
tmp = now | gp_nxt[p];
tmp2 = now | na_nxt[p];
if (!vis[tmp])
chess[p] = 'O', vis[tmp]++, dfs(tmp);
chess[p] = '.';
if (!vis[tmp2])
chess[p] = 'X', vis[tmp2]++, dfs(tmp2);
chess[p] = '.';
if (!gp_vis[now]) ans_gp[now] = ans_na[tmp], gp_vis[now]++;
//else ans_gp[now] = max(ans_gp[now], ans[tmp]);
if (!na_vis[now]) ans_na[now] = ans_gp[tmp2], na_vis[now]++;
//else ans_na[now] = min(ans_na[now], ans[tmp2]);
ans_gp[now] = max(ans_gp[now], ans_na[tmp]);
ans_na[now] = min(ans_na[now], ans_gp[tmp2]);
}
if (flag) ans_na[now] = score(), ans_gp[now] = ans_na[now];
}
void yuchuli() {
gp_nxt[0] = 1LL, na_nxt[0] = 2LL;
for (int i = 1; i < 9; i++)
gp_nxt[i] = gp_nxt[i - 1] << 2, na_nxt[i] = na_nxt[i - 1] << 2;
dfs(0LL);
}
void solve() {
int type; si(type);
for (int i = 1, cnt = 0; i <= 3; i++) {
char cao[4];
scanf("%s", cao);
chess[cnt++] = cao[0];
chess[cnt++] = cao[1];
chess[cnt++] = cao[2];
}
if (type)
printf("%d\n", ans_gp[trs_x()]);
else
printf("%d\n", ans_na[trs_x()]);
}
int main() {
int t;
yuchuli();
si(t);
while (t--)
solve();
return 0;
}
/*
2
1
.XX
O.X
XOX
0
OOO
OOO
OOO
*/
V. 生日蛋糕
题意:体积n× π的m层蛋糕, 每层都是圆柱体,在下面的蛋糕的半径和高度(都是整数)都要大于在上面的蛋糕,蛋糕的表面积Q=S× π ,对于给定的n和m求蛋糕的最小S(无解输出0)
搜索剪枝:考虑到蛋糕的半径和高度都是整数,也就是说m层蛋糕的最小体积是可以计算得到的。dfs扫所有可行的半径和高度,其中半径和高度的max取上一层的半径和高度减一,最小值为m减当前层数+1。
对于剪枝,如果当前的体积加上剩余层数的最小值已经大于n了,说明体积n× π 的蛋糕做不出就退出。每次搜到一个结果就更新ans,如果当前面积加剩余层数的最小面积大于ans就退出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const int N = 2e6 + 10;
//int n, m;
int n, m;
int ans=114514;
int rest[10010];
void REST() { for (int i = m, j=0; i >= 1; i--) rest[++j] = i * i * (i + 1) * (i + 1) / 4; }
void dfs(int u, int s_pre, int v_pre, int r_last, int h_last, int s_ground) {
int r_max = r_last-1, h_max=h_last-1;
int r_min = 1 + m - u, h_min = r_min;
if (u == m) {
if (r_last == 1 || h_last == 1)
return;
for (int i = r_max, v = n - v_pre; i >= r_min; i--)
for (int j = h_max; j >= h_min; j--)
if (i * i * j == v)
ans = m == 1 ? min(ans, i * i + s_pre + 2 * i * j) : min(ans, s_ground + s_pre + 2 * i * j);
return;
}
for(int i = r_max;i>=r_min;i--)
for (int j = h_max; j >= h_min; j--) {
if (i * i * j + rest[u + 1] + v_pre > n) continue;
if (u == 1) {
if (i * i + s_pre + 2 * ((n - v_pre) / r_max) >= ans) continue;
}
else if(s_ground+s_pre + 2 * ((n - v_pre) / r_max) >= ans)
continue;
if (u == 1) s_ground = i * i;
dfs(u + 1, s_pre + 2 * i * j, v_pre + i * i * j, i,j, s_ground);
}
}
void solve() {
sii(n, m);
REST();
if (rest[1] > n)
printf("%d", 0);
else {
dfs(1, 0, 0, sqrt(n)+10000, sqrt(n)+10000, 114514);
if (ans == 114514)
printf("%d", 0);
else
printf("%d", ans);
}
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
/*
*/
W. 樱之刻
题意:先给出$n$个字符串$s_i$,最后给出文本$t$,求每个$s_i$在$t$中的出现次数
AC自动机
对$n$个字符串$s_i$建字典树,$tri[u][c]$表示节点$u$后加一个字符$c$的节点,$u$—$c$→$v$。同时还要记录一下第$i$个字符串的末尾还是最近哪一次出现过的字符串的末尾(如果有的话,$nxt[]$储存),再记录一下节点u是否是某模式串末尾($id[]$储存)
建$fail$树. 对状态$u$,它的$fail$指针指向$u$的最长后缀$v$
操作:
初始化:遍历$26$个字母$i$,如果$tri[0][i]$存在,向队列$q$中添加$tri[0][i]$;
扫队列$q$:设$u$为当前处理节点,如果$tri[u][i]$存在,$fail[tri[u][i]]=tri[0][i]$,同时把$tri[u][i]$塞到队列$q$里。然后$tri[u][i]=tri[fail[u]][i]$. 这样做确实是改变了字典树的结构,但事实上我们在跳$fail$指针时只关心能不能匹配到一个串的后缀,所以直接改变了$tri[u][i]$到$tri[fail[u]][i]$不会影响$fail$指针的匹配,还能起到压缩跳$fail$指针路径的作用;
1 2 3 4 5 6 7 8 9 10 11
while (l<=r) {//数组模拟队列 int u = q[l++]; for (int i = 0; i < 26; i++) { if (tri[u][i]) { fail[tri[u][i]] = tri[fail[u]][i]; q[++r]=tri[u][i]; continue; } tri[u][i]= tri[fail[u]][i]; } }
统计出现次数
首先一层$for$扫文本串,初始化得到一个不完整的$sum$数组:
1
for (int i = 0, u = 0; i < len; i++) u = tri[u][s[i] - 'a'], sum[u]++;
然后就体会到建树时用数组模拟队列的优点了,逆序队列就是处理树上差分的顺序,此时利用前面保存的$nxt$数组和$id$数组就能对树上的每个节点$u$,快速遍历以该节点为末尾的所有模式串(这个过程更新$ans$数组)。逆序遍历字典树能保证父节点统计到所有后代的值,类似前面回文自动机的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const int N = 2e6 + 10;
int n, m;
int cnt, ans_id, l = 1, r;
int ans[N], fail[N], sum[N], id[N],NEXT[N];
int tri[N][27];
char tmp[N];
int q[N];
void get_fail() {
for (int i = 0; i < 26; i++)
if (tri[0][i])q[++r]=(tri[0][i]);
while (l<=r) {
int u = q[l++];
for (int i = 0; i < 26; i++) {
if (tri[u][i]) {
fail[tri[u][i]] = tri[fail[u]][i];
q[++r]=tri[u][i];
continue;
}
tri[u][i]= tri[fail[u]][i];
}
}
}
void match(char s[]) {
int u = 0, len = strlen(s);
for (int i = 0; i < len; i++)
u = tri[u][s[i] - 'a'], sum[u]++;
for (int i = cnt; i >= 1; i--) {
for (int j = id[q[i]]; j; j = NEXT[j])
ans[j] = sum[q[i]];
sum[fail[q[i]]] += sum[q[i]];
}
}
void build(char s[]) {//建字典树
int len = strlen(s), u = 0;
for (int i = 0; i < len; i++) {//遍历字符串
int c = s[i] - 'a';
if (!tri[u][c])
tri[u][c] = ++cnt;//没找到
u = tri[u][c];
}
ans_id++;
NEXT[ans_id] = id[u];
id[u] = ans_id;
}
void solve() {
si(n);
for (int i = 1; i <= n; i++) {
scanf("%s", tmp);
build(tmp);
}
get_fail();
scanf("%s", tmp);
match(tmp);
for (int i = 1; i <= n; i++)printf("%d\n", ans[i]);
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
/*
3
what
is
mind
thisisnotmind
*/
X. APM
题意:给定一个字符串,取其一个前缀和一个后缀(都可以为空且不能重合),要求拼接后的字符串为回文串,求这个拼接字符串的最长长度
分析:对于这个拼接出来的字符串,可以分解成两部分:原字符串长度相等的前后缀+拼接字符串的剩余部分
所以先一一对原字符串匹配前后缀,然后对中间部分跑$Manacher$,取前端或后端遇到边界的回文子串的最大长度,加上之前匹配得到的前后缀长度即为答案。
Manacher
首先考虑将回文串分成奇偶长度两种情况,设$d_1[i]$是以$i$为中心的最长回文长度的半径长度且长度为奇数,$d_2[i]$为偶数,先考虑奇数情况:我们记录已找到的子回文串中右边界最大的那个串的左右边界$l$、$r$,那么当我们处理到$d_1[i]$时,有下面三种情况:
- 若$i>r$,我们暴力寻找该位置的$d_1[i]$,并更新$l$、$r$
- 若$i<=r$,在$l$和$r$范围足够大的情况下,会出现下面的情况:
图来自OI Wiki
即以$s_i$为中心的回文子串一定会和以$s_j$为中心的回文子串完全一致,而这个$j$是可以直接计算得到的,此时$d_1[i]=d_1[j]$
- 在$2$的基础上,如果以$j$为中心的串太大了,超出边界$l$、$r$,可以先取$r$作$d_`[i]$的值,然后从$r$处暴力匹配更新$d_1[i]$
$d_2[i]$的处理同理
最后可以把奇偶情况统一起来:对字符串$s$,比如$abcd$,我们将其插入分隔符#变成$#a#b#c#d#$,然后正常跑之前的奇数情况,此时的$d_1[i]$为在s中以对应位置为中心的回文串的长度加一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const int N = 2e7 + 10;
int n, m;
int ans = 0, l_, r_;
char s[N];
char s_[N];
int d1[N], d2[N];
void Manacher() {
s_[0] = '#';
int cnt = 1;
for (int i = l_; i <= r_; i++)
s_[cnt++] = s[i], s_[cnt++] = '#';
for (int i = 0, l = 0, r = -1; i < cnt; i++) {//奇数,j取l+r-i
int j = l + r - i;
int k = (i > r) ? 1 : min(r - i + 1, d1[j]);
while (i - k >= 0 && i + k < cnt && s_[i - k] == s_[i + k]) k++;
d1[i] = k--;
if (i - k == 0 || i + k == cnt-1)
ans = max(ans, d1[i] - 1);//左右端取到底了
if (i + k > r)
l = i - k, r = i + k;
}
}
void solve() {
scanf("%s", s);
int len = strlen(s);
l_ = 0, r_ = len - 1;
while (l_< r_)
if (s[l_] == s[r_])
l_++, r_--;
else break;
if (l_ >= r_)
printf("%d", len);
else {
Manacher();
printf("%d", ans+l_*2);
}
}
int main() {
int t;
/*si(t);
while (t--)*/
solve();
return 0;
}
/*
*/