字符串笔记
字符串知识点
字符串基本概念
定义
Border
字符串 \(S\) 的同长度前缀和后缀完全相同, \(Prefix[i] = Suffix[i] <=> S[1,p] == S[|S|-p+1, |S|]\) ,则称为 \(Border\) ,字符串本身可以是自己的 \(Border\) 根据情况判断。
- \(Prefix[i]\) 的 Border 长度减 \(1\) 是 \(Prefix[i - 1]\) 的 Border 长度,反之不一定成立, 需要检验后一个字符是否相等。
周期
- 对于字符串 \(S\) 和正整数 \(p\) ,如果有 \(S[i] = S[i - p]\) ,对于 \(p < i \leq |S|\) 成立,则 \(p\) 为字符串的一个周期。
- 当然,\(p=|S|\) 一定是 \(S\) 的周期
循环节
- \(p\) 是字符串 \(S\) 的周期,满足 \(p \;|\; |S|\) ,则 \(p\) 是 \(S\) 的一个循环节。
- 当然,\(p = |S|\) 是 \(S\) 的循环节
性质
- \(p\) 是 \(S\) 的周期等价于 \(|S| - p\) 是 \(S\) 的 Border。
- 即字符串周期性质等价于 Border 性质,注意 Border不具有二分性。
- Border 具有传递性,即 Border 的 Border 也是字符串的 Border。
- 即求字符串的所有 Border 等价于求所有前缀的最大Border。
KMP
Next数组
- \(ne[i] = Prefix[i]\) 的非平凡最大 Border,在前缀里找 Border。
- \(ne[1] = 0\)
- 求 \(Prefix[i]\) 的所有长度大于 1 的 Border。去掉最后一个字母就变成 \(Prefix[i - 1]\) 的Border
- 故求 \(ne[i]\) 的时候,遍历 \(Prefix[i - 1]\) 的所有 Border,即 \(ne[i - 1], ne[ne[i - 1]], ... , 0\)。检查最后一个字符是否等于 \(S[i]\)
Border树
对于字符串 \(S\), \(n = |S|\),它的 Border 树 (next 树) 共有 \(n+1\) 个节点:\(0, 1, 2, 3,..,n\), \(0\) 是这颗有向树的根。对于其他节点父节点为 \(ne[i]\)
性质
- 每个前缀 \(Prefix[i]\) 的所有 Border ,就是节点 \(i\) 到根的链。
- 哪些前缀有长度为 \(x\) 的 Border,等价于 \(x\) 的子树
- 求两个前缀的公共 Border,等价于求两个节点的 \(LCA\)
字符串哈希
Trie
字典:一个字符串的集合
字典串:在字典里的串
01-Trie
题目多于普通 Trie,也更难。
异或两种做法:
- 线性基
- 01-Trie
可持久化 Trie
可持久化思想:将修改操作在新的节点,不对原节点修改
AC自动机
鸽
Manacher
字符串好题
KMP
差分 + KMP
题意
有一个长度为 \(n\) 的数列 \(A\) ,一个长度为 \(m\) 的数列 \(B\) ,现在询问 \(A\) 中有多少个长度为 \(m\) 的连续子序列 \(A'\),
满足 \((a_1'+b_1)\%k = (a_2'+b_2)\%k = ... = (a_m' + b_m)\%k\)。
思路
- 化简式子,利用差分得 \((a_1' - a_2') \% k = (b_2 - b_1) \% k \dots (a_{m-1}' - a_m') \% k = (b_m - b_{m-1}) \% k\)
- 差分后序列长度均减 \(1\) ,首尾不能相连,然后进行 KMP 算法
- 注意模意义下相同需要相减 \(\% k ==0\)
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 2e5 + 10;
int ta[N], tb[N], n, m, k, a[N], b[N];
int ne[N], ans;
void get_ne(int* s){
int len = m;
ne[1] = ans = 0;
for(int i = 2, j = 0; i <= len; i++){
while(j && (s[j + 1] - s[i]) % k) j = ne[j];
if((s[i] - s[j + 1]) % k == 0) j++;
ne[i] = j;
}
}
void Match(int s[], int p[]){
int lens = n, lenp = m;
for(int i = 1, j = 0; i <= lens; i++){
while(j && (s[i] - p[j + 1]) % k ) j = ne[j];
if((s[i] - p[j + 1]) % k == 0) j++;
if(j == lenp){
ans ++;
j = ne[j];
}
}
}
int sub(int x, int y){
return (x - y + k) % k;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while(T--){
cin >> n >> m >> k;
for(int i = 1; i <= n; i++)
cin >> ta[i];
for(int i = 1; i <= m; i++)
cin >> tb[i];
for(int i = 1; i <= n - 1; i++){
a[i] = sub(ta[i], ta[i % n + 1]);
}
for(int i = 1; i <= m - 1; i++){
b[i] = sub(tb[i % m + 1], tb[i]);
}
get_ne(b);
n--, m--;
Match(a, b);
cout << ans << endl;
}
return 0;
}
KMP 求矩阵最小公共循环周期 + 单调队列
题意
给你一个 \(n*m\) 的矩阵,每个格子有自己的颜色和权值,现在要你选择一个子矩阵,假设子矩阵的大小是 \(p*q\) 的,
子矩阵选择的条件是,将子矩阵无限的平移复制粘贴,原来的 \(n*m\) 的矩阵是复制粘贴之后矩阵的子矩阵,
选择子矩阵有一个花费,花费是原矩阵所有的大小为 \(p*q\) 的子矩阵中选择一个最大值 \(x\) ,花费就是 \(x*(p + 1)*(q + 1)\) 。
数据范围
\(n*m<=1e6\)。
思路
Solution
#include<bits/stdc++.h>
typedef long long ll;
#define endl "\n"
using namespace std;
const int N = 1e6 + 100;
string s[N];
int mp[N], q[N], n, m, ne[N];
void get_ne(string s){
int len = s.size() - 1;
ne[1] = 0;
for(int i = 2, j = 0; i <= len; i++){
while(j && s[j + 1] != s[i]) j = ne[j];
if(s[i] == s[j + 1]) j++;
ne[i] = j;
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> s[i];
s[i] = " " + s[i];
}
vector<vector<int>> c(n + 1, vector<int>(m + 1, 0));
vector<vector<int>> val(n + 1, vector<int>(m + 1, 0));
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> c[i][j];
int x = n, y = m;
for(int i = 1; i <= n; i++){
get_ne(s[i]);
int t = m;
while(ne[t]){
mp[m - ne[t]] ++;
if(mp[m - ne[t]] == n){
y = min(m - ne[t], y);
}
t = ne[t];
}
}
memset(mp, 0, sizeof mp);
for(int i = 1; i <= m; i++){
string t(n + 1, 0);
for(int j = 1; j <= n; j++)
t[j] = s[j][i];
get_ne(t);
int tmp = n;
while(ne[tmp]){
mp[n - ne[tmp]] ++;
if(mp[n - ne[tmp]] == m)
x = min(n - ne[tmp], x);
tmp = ne[tmp];
}
}
for(int i = 1; i <= n; i++){
int hh = 0, tt = -1;
for(int j = 1; j < y; j++){
while(hh <= tt && c[i][q[tt]] <= c[i][j]) tt--;
q[++tt] = j;
}
for(int j = y; j <= m; j++){
if(hh <= tt && j - q[hh] >= y) hh++;
while(hh <= tt && c[i][q[tt]] <= c[i][j]) tt--;
q[++tt] = j;
val[i][j] = c[i][q[hh]];
}
}
ll ans = 2e9;
for(int i = y; i <= m; i++){
int hh = 0, tt = -1;
for(int j = 1; j < x; j++){
while(hh <= tt && val[q[tt]][i] <= val[j][i]) tt--;
q[++tt] = j;
}
for(int j = x; j <= n; j++){
if(hh <= tt && j - q[hh] >= x) hh++;
while(hh <= tt && val[q[tt]][i] <= val[j][i]) tt--;
q[++tt] = j;
ans = min(ans, 1ll * val[q[hh]][i]);
}
}
cout << 1ll * (x + 1) * (y + 1) * ans << endl;
return 0;
}
Trie
Trie + 拓扑排序判环
题意
给定 \(n\) 个字符串,互不相等,你可以任意指定字符之间的大小关系(即重定义字典序),求有多少个串可能成为字典序最小的串,并输出它们
思路
- 首先建立所有字典串的 Trie 树,然后考虑什么时候不能字典序最小
- 存在一个字典串是另一个字典串的前缀,那么后者不可能字典序最小
- 寻找该字典串时,节点的其他路径所达到的节点字符字典序必须大于目标路径所到达节点
- 前缀可对路径节点标记判断得解,字典序的大小关系可以建立DAG,再用拓扑图判环,有环则不合法,并注意并查集并不能实现DAG判环
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 3e5 + 10;
int n, son[N][26], cnt[N], g[26][26], idx, indeg[26];
vector<string> ans;
void insert(string s){
int p = 0;
for(auto c: s){
int u = c - 'a';
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;
}
bool check(){
queue<int> q;
for(int i = 0; i < 26; i++){
if(!indeg[i])
q.push(i);
}
while(q.size()){
int t = q.front();
q.pop();
for(int i = 0; i < 26; i++){
if(g[t][i]){
indeg[i] -= g[t][i];
g[t][i] = 0;
if(!indeg[i])
q.push(i);
}
}
}
for(int i = 0; i < 26; i++)
if(indeg[i])
return false;
return true;
}
bool query(string s){
int p = 0;
memset(g, 0, sizeof g);
memset(indeg, 0, sizeof indeg);
for(auto c: s){
int u = c - 'a';
if(cnt[p]) return false;
for(int i = 0; i < 26; i++){
if(son[p][i] && i != u){
g[u][i] ++;
indeg[i]++;
}
}
p = son[p][u];
}
bool fl = check();
return fl;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
vector<string> v;
for(int i = 0; i < n; i++){
string s;
cin >> s;
insert(s);
v.pb(s);
}
for(auto s: v){
if(query(s)) ans.pb(s);
}
cout << ans.size() << endl;
for(auto t: ans)
cout << t << endl;
return 0;
}
AC自动机
模拟 + 字符集特殊处理
题意
模拟题
思路
- 根据题意模拟,然后跑一遍AC自动机
- 一定注意 \(8\) 个 bit 转换成整数,可能出现比 \(128\) 大或者需要对
\0
特殊处理,均转换为 int 存储。
Solution
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<queue>
#include<map>
#include<vector>
#include<iomanip>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
int n, m;
string int2bin(int x){
string res;
for(int i = 5; i >= 0; i--)
res += char(((x >> i) & 1) + '0');
return res;
}
string char2bin(char c){
if(c >= 'A' && c <= 'Z') return int2bin(c - 'A');
if(c >= 'a' && c <= 'z') return int2bin(26 + c - 'a');
if(c >= '0' && c <= '9') return int2bin(52 + c - '0');
if(c == '+') return int2bin(62);
if(c == '/') return int2bin(63);
return "";
}
const int N = 520, M = 520 * 64, S = 256;
bool vis[M];
int cnt[M], tr[M][S], idx, fail[M];
void insert(vector<int> s){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i];
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
cnt[p]++;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front();
q.pop();
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
int query(vector<int> s){
memset(vis, 0, sizeof vis);
int u = 0, res = 0;
for(int i = 0; i < s.size(); i++){
u = tr[u][s[i] - 0];
for(int j = u; j && !vis[j]; j = fail[j]){
res += cnt[j], vis[j] = true;
}
}
return res;
}
void init(){
idx = 0;
memset(tr, 0, sizeof tr);
memset(fail, 0, sizeof fail);
memset(cnt, 0, sizeof cnt);
}
string str2bin(string ori){
int t = 0;
string res = "";
for(int j = 0; j < ori.size(); j++){
char c = ori[j];
if(c != '='){
res += char2bin(c);
}
else
t++;
}
res = res.substr(0, res.size() - 2 * t);
return res;
}
vector<int> bin2asc(string ori){
vector<int> s;
for(int i = 0; i < ori.size(); i += 8){
int res = 0;
for(int j = i; j < i + 8; j++){
char c = ori[j];
int d = j - i;
if(c == '1')
res += 1 << (7 - d);
}
s.pb(res);
}
return s;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
bool fl = false;
while(cin >> n){
init();
for(int i = 0; i < n; i++){
string s;
cin >> s;
s = str2bin(s);
vector<int> res = bin2asc(s);
insert(res);
}
build();
cin >> m;
while(m--){
string s;
cin >> s;
s = str2bin(s);
vector<int> res = bin2asc(s);
cout << query(res) << endl;
}
cout << endl;
}
return 0;
}
AC自动机DAG + 矩阵快速幂
题意
给定 \(m\) 个字符串(仅含有 \(A T C G\) ,长度不超过 \(10\) ),和正整数 \(n\) ,问有多少种仅包含 \(A T C G\) 的长度为 \(n\) 的字符串,满足不包含这 \(m\) 个字符串中的任意一个作为子串。
数据范围
\(1\leq m \leq 10\)
\(1\leq n \leq 2e9\)
思路
- 利用建立AC自动机后可以得到一个有向图,点权(或边权)是字符集,将问题转化。
- 转化后问题等价于,在有向图上找出一条从 \(0\) 出发经过 \(n\) 条边的合法路径的方案数,合法路径上不含病毒,即不包含 \(Fail\) 树上所有病毒点的子树节点。
- 经过上述转化后如果你对矩阵快速幂熟悉的话,会发现这是矩阵快速幂的一个经典问题, 最后套用矩阵快速幂,统计 \(0\) 到所有点的方案总数即可。
- 定义 \(M[i][j]\) 为从 \(i\) 到 \(j\) 的路径数,那么经过两条边的方案数就是 \(M[i][j]^2\)。
- 当然,经过 \(n\) 条边的方案数就是 \(M[i][j]^n\)
- 矩阵记得开
long long
Solution
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<queue>
#include<map>
#include<vector>
#include<iomanip>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 101, M = 15 * N, S = 4, mod = 1e5;
int tr[M][S], idx, fail[M];
ll n, m;
bool st[M];
int get(char c){
if(c == 'A')
return 0;
if(c == 'T')
return 1;
if(c == 'C')
return 2;
return 3;
}
void insert(string s){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = get(s[i]);
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
st[p] = true;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
int u = q.front();
q.pop();
if(st[fail[u]])
st[u] = true;
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
template <typename T, size_t N> struct Mat {
int len;
Mat() { memset(data, 0, sizeof(data)); len = idx + 1; }
T *operator[](int i) { return data[i]; }
const T *operator[](int i) const { return data[i]; }
T add(T a, T b){
return (a + b) % mod;
}
Mat &operator += (const Mat &o) {
for (int i = 0; i < len; ++i)
for (int j = 0; j < len; ++j)
data[i][j] = add(data[i][j], o[i][j]);
return *this;
}
Mat operator + (const Mat &o) const {
return Mat(*this) += o;
}
Mat &operator -= (const Mat &o) {
for (int i = 0; i < len; ++i)
for (int j = 0; j < len; ++j)
data[i][j] = add(data[i][j], -o[i][j]);
return *this;
}
Mat operator-(const Mat &o) const {
return Mat(*this) -= o;
}
Mat operator*(const Mat &o) const {
static T buffer[N];
Mat result;
for (int j = 0; j < len; ++j) {
for (int i = 0; i < len; ++i)
buffer[i] = o[i][j];
for (int i = 0; i < len; ++i)
for (int k = 0; k < len; ++k)
result[i][j] += (data[i][k] * buffer[k]) % mod;
}
return result;
}
Mat power(unsigned long long k) const {
Mat res;
for (int i = 0; i < len; ++i)
res[i][i] = T{1};
Mat a = *this;
while (k) {
if (k & 1ll)
res = res * a;
a = a * a;
k >>= 1ll;
}
return res;
}
private:
T data[N][N];
};
template <typename T, size_t N>
void get_mat(Mat<T, N>& g){
for(int u = 0; u <= idx; u++){
for(int j = 0; j < 4; j++){
if(!st[u] && !st[tr[u][j]])
g[u][tr[u][j]] ++;
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> m >> n;
for(int i = 0; i < m; i++){
string s;
cin >> s;
insert(s);
}
build();
Mat<ll, N> ans;
get_mat(ans);
ans = ans.power(n);
ll res = 0;
for(int i = 0; i <= idx; i++){
res = (res + ans[0][i]) % mod;
}
cout << res % mod << endl;
return 0;
}
AC自动机 + 矩阵快速幂(矩阵分块,矩阵快速幂求解等比数列)
题意
求包含至少一个字典串长度至多为 \(n\) 的字符串个数,大体与上题类似
思路
- 与上体思路相同,但此题求长度小于等于 \(n\) 的所有方案,考虑取补集,总方案数 - 不包含任意字典串的字符串个数 = 答案
- 显然总方案数为 \(26 + 26^2 + 26^3 +...+ 26^n\) ,遗憾的是这里最好不用等比数列来解,考虑矩阵快速幂
-
构造矩阵 \(A = \begin{bmatrix} 26 & 1 \\ 0 & 1 \\ \end{bmatrix}\)
-
则 \(A^n = \begin{bmatrix} 26^n & 26^{n - 1} + 26^{n - 2} + \cdots + 1\\ 0 & 1 \end{bmatrix}\)
-
取第一行元素和为 \(A^n + A^{n - 1} + A^{n - 2} + \cdots + 1\) ,减 \(1\) 便是所求总方案数
-
- 对于不包含任意字符串的个数,由上一题可知,长度固定的情况下是好求的,其实长度小于 \(n\) 的方案数就是 \(G + G^2 +\cdots + G^n\) ,矩阵的幂次和,那么如何求解矩阵幂次和呢?
- 神奇的是,我们只需要将上述矩阵 \(A\) 中 \(26\) 替换为矩阵 \(G\) 即可,其中 \(1\) 替换为单位阵
- 最后遍历整个矩阵 \(G[0][i]\) 的和减 \(1\) 就是不包含任意字典串的字符串个数。
- 小细节,对 \(2^{64}\) 取模,只需要将数据类型改为
unsigned long long
。
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int M = 31, S = 26;
int tr[M][S], idx, fail[M];
bool st[M];
int n, m;
void insert(string s){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i] - 'a';
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
st[p] = true;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front();
q.pop();
if(st[fail[u]]) st[u] = true;
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
template <typename T, size_t N> struct Mat {
int len;
Mat() { memset(data, 0, sizeof(data)); len = N; }
T *operator[](int i) { return data[i]; }
const T *operator[](int i) const { return data[i]; }
T add(T a, T b){
return a + b;
}
Mat &operator += (const Mat &o) {
for (int i = 0; i < len; ++i)
for (int j = 0; j < len; ++j)
data[i][j] = add(data[i][j], o[i][j]);
return *this;
}
Mat operator + (const Mat &o) const {
return Mat(*this) += o;
}
Mat &operator -= (const Mat &o) {
for (int i = 0; i < len; ++i)
for (int j = 0; j < len; ++j)
data[i][j] = add(data[i][j], -o[i][j]);
return *this;
}
Mat operator-(const Mat &o) const {
return Mat(*this) -= o;
}
Mat operator*(const Mat &o) const {
static T buffer[N];
Mat result;
for (int j = 0; j < len; ++j) {
for (int i = 0; i < len; ++i)
buffer[i] = o[i][j];
for (int i = 0; i < len; ++i)
for (int k = 0; k < len; ++k)
result[i][j] += (data[i][k] * buffer[k]);
}
return result;
}
Mat power(unsigned long long k) const {
Mat res;
for (int i = 0; i < len; ++i)
res[i][i] = T{1};
Mat a = *this;
while (k) {
if (k & 1ll)
res = res * a;
a = a * a;
k >>= 1ll;
}
return res;
}
private:
T data[N][N];
};
template <typename T, const int N>
Mat<T, N> get_mat(){
Mat<T, N> g;
for(int i = 0; i <= idx; i++){
for(int j = 0; j < 26; j++){
if(!st[i] && !st[tr[i][j]])
g[i][tr[i][j]] ++;
}
}
return g;
}
void init(){
idx = 0;
memset(tr, 0, sizeof tr);
memset(fail, 0, sizeof fail);
memset(st, 0, sizeof st);
}
ull qmi(ull a, ull k){
ull res = 1;
while(k){
if(k & 1ull)
res = res * a;
a = a * a;
k >>= 1ull;
}
return res;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
while(cin >> m >> n){
init();
for(int i = 0; i < m; i++){
string s;
cin >> s;
insert(s);
}
build();
auto res = get_mat<ull, M>();
Mat<ull, 2 * M> a;
int len = idx + 1;
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
a[i][j] = res[i][j];
}
a[i][i + len] = 1;
}
for(int i = len; i < 2 * len; i++){
for(int j = 0; j < len; j++){
a[i][j] = 0;
}
a[i][i] = 1;
}
a = a.power(n);
ull ans = 0;
for(int i = 0; i < 2 * len; i++){
ans += a[0][i];
}
ans--;
Mat<ull, 2> b;
b[0][0] = 26, b[0][1] = 1, b[1][1] = 1;
b = b.power(n);
ull sum = b[0][0] + b[0][1] - 1;
cout << sum - ans << endl;
}
return 0;
}
AC自动机 + 状态压缩DP
题意
给了 \(m\) 个字典串,问你能构造多少种长度为 \(n\) 的字符串,满足至少包含 \(k\) 个不同的字典串。
数据范围
\(1\leq n\leq 25, 1 \leq m \leq 10, 1\leq k\leq10\)
答案对 \(20090717\) 取模
思路
- DP不难想到,个人做的难点是想到使用状压 DP 在 Trie 图上跑。
- \(f[i][j][s]\) 表示当前长度为 \(i\),停留在 \(j\) 点,经过的字典串状态为 \(s\) 的方案数。
- \(f[i][v][nxt] = \Sigma f[i - 1][u][s | st[v]]\),\(u\) 是 \(v\) 父节点,若
(st[v] >> id) & 1 == 1
表明在节点 \(v\) 包含了编号为 id 的字典串 - 统计答案,在各点所有状态上加和,判断状态经过字典串个数是否大于 \(k\) 即可。
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int M = 110, S = 26, mod = 20090717;
int tr[M][S], idx, fail[M];
int st[M];
ll f[26][M][1 << 10];
void insert(string s, int x){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i] - 'a';
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
st[p] |= 1 << x;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front();
q.pop();
st[u] |= st[fail[u]];
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void init(){
idx = 0;
memset(tr, 0, sizeof tr);
memset(st, 0, sizeof st);
memset(fail, 0, sizeof fail);
memset(f, 0, sizeof f);
}
int add(int a, int b){
return (a + b) % mod;
}
int num[1 << 10];
int get(int x){
int res = 0;
while(x){
if(x & 1) res++;
x >>= 1;
}
return res;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m, k;
for(int i = 0; i < 1 << 10; i++){
num[i] = get(i);
}
while(cin >> n >> m >> k, n || m || k){
init();
for(int i = 0; i < m; i++){
string s;
cin >> s;
insert(s, i);
}
build();
f[0][0][0] = 1;
for(int i = 1; i <= n; i++)
for(int u = 0; u <= idx; u++)
for(int s = 0; s < 1 << m; s ++){
if(f[i - 1][u][s])
for(int t = 0; t < S;t ++){
int v = tr[u][t];
int nxt = s | st[v];
f[i][v][nxt] = add(f[i][v][nxt], f[i - 1][u][s]);
}
}
ll ans = 0;
for(int i = 0; i <= idx; i++){
for(int j = 0; j < 1 << m; j++){
if(num[j] >= k)
ans = add(ans, f[n][i][j]);
}
}
cout << ans << endl;
}
return 0;
}
AC自动机-DAG + DP 输出方案
题意
给出 \(n\) 个字符串(仅包含英文字母),每个字符串都有一个权值,现在问在组成字符串长度不超过 \(m\) 的前提下,
怎样构造才能使得出现的字符串权值和最大,在满足上个条件的基础上长度最短,如果依然有多个答案,输出字典序最小的答案
数据范围
\(n\leq 50, m\leq 100, w \leq 100\)
思路
- 容易想到在建立 AC 自动机时给字典串结尾加上权值,然后按照长度为阶段,节点为第二维度跑DP,还有个麻烦点方案输出
- 对于最终构造的方案输出同时维护一个 string 数组,当权值能被更新 string 数组一定更新,另外的两种情况对字符串比较符重载是一个很好的方法
- 状态表示:
f[i][j] 表示目前长度为 i 停留在节点 j 的权值最大值, path[i][j] 为对应的字符串
- 状态转移:
j = tr[u][k], f[i][v] = max{f[i - 1][u] + w[v]}, path[i][j] = path[i - 1][u] + char('a' + k)
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int M = 1010, S = 26;
int w[M], tr[M][S], idx, fail[M], f[60][M];
string path[60][M];
void insert(string s, int val){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i] - 'a';
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
w[p] += val;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front();
q.pop();
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void init(){
idx = 0;
memset(tr, 0, sizeof tr);
memset(fail, 0, sizeof fail);
memset(w, 0, sizeof w);
memset(f, -0x3f, sizeof f);
}
bool cmp(string& a, string& b){
if(a.size() != b.size()) return a.size() < b.size();
return a < b;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while(T--){
init();
int n, m;
vector<pair<string, int>> v;
cin >> n >> m;
for(int i = 0; i < m; i++){
string s;
cin >> s;
v.pb({s, i});
}
for(int i = 0; i < m; i++){
cin >> v[i].y;
insert(v[i].x, v[i].y);
}
build();
path[0][0] = "";
f[0][0] = 0;
string ans;
int mx = 0;
for(int i = 0; i < n; i++)
for(int u = 0; u <= idx; u++)
if(f[i][u] >= 0)
for(int k = 0; k < S; k++){
int v = tr[u][k];
int t = f[i][u];
string nxt = path[i][u] + char('a' + k);
t += w[v];
if(t > f[i + 1][v] || (t >= f[i + 1][v] && cmp(nxt, path[i + 1][v]))){
f[i + 1][v] = t;
path[i + 1][v] = nxt;
if(f[i + 1][v] > mx || (f[i + 1][v] >= mx && cmp(path[i + 1][v], ans))){
mx = f[i + 1][v];
ans = path[i + 1][v];
}
}
}
cout << ans << endl;
}
return 0;
}
AC自动机DAG + 最短路DP
题意
让你求从 \(1\) 走到 \(n\) 的最短路,但是有 \(m\) 条长度为 \(k_i\) 路径是不能走的,且走到每次走只能走比当前点大的点
数据范围
\(2 \leq n \leq 50\)
\(1 \leq m \leq 100\)
\(2 \leq k \leq 5\)
思路
- 一些路径不能走,这对应 \(m\) 个字典串,然后考虑在AC自动机Trie图上跑 最短路 or DP
- 最短路算法明显跑不出来,考虑如何 DP。可以当成经典的套路吧(也许),。
- 状态表示: \(dp[i][j]\) 编号 \(i\) 的点停留在自动机节点 \(j\) 的最短路,这样可以更好的统计答案和状态转移
- 状态转移如何进行,枚举转移起点编号和终点编号,转移终止节点由转移起始节点决定。
- 状态转移:just like this
dp[1][tr[0][1]] = 0; // dp[i][j] 表示编号 i 停留在节点 j 的最短距离
for(int i = 1; i <= n; i++){
for(int u = 0; u <= idx; u++){
if(dp[i][u] >= INF) continue;
for(int k = i + 1; k <= n; k++){ // 只能走编号更大的点。
int v = tr[u][k];
if(st[v]) continue;
dp[k][v] = min(dp[k][v], dp[i][u] + get(i - 1, k - 1));
}
}
}
- 细节:有 int 范围内数相减的情况,考虑开 double 或者 long long。否则最后的数会爆 int
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int M = 1010, S = 55;
const double INF = 2e18;
int cnt[M], tr[M][S], idx, fail[M];
vector<pair<double, double>> pos;
bool st[M];
double dp[S][M];
void insert(vector<int> s){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i];
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
st[p] = true;
}
double get(int a, int b){
double dx = pos[a].x - pos[b].x, dy = pos[a].y - pos[b].y;
return sqrt(dx * dx + dy * dy);
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front();
q.pop();
if(st[fail[u]])
st[u] = true;
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void init(){
idx = 0;
memset(st, 0, sizeof st);
memset(tr, 0, sizeof tr);
memset(fail, 0, sizeof fail);
pos.clear();
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
while(cin >> n >> m, n || m){
init();
for(int i = 0; i < n; i++){
double x, y;
cin >> x >> y;
pos.pb({x, y}); // 坐标直接写成double,否则两个int范围数相减会溢出
}
while(m--){
int k;
cin >> k;
vector<int> tmp;
while(k--){
int x;
cin >> x;
tmp.pb(x);
}
insert(tmp);
}
build();
for(int i = 1; i <= n; i++)
for(int j = 0; j <= idx; j++)
dp[i][j] = INF;
dp[1][tr[0][1]] = 0; // dp[i][j] 表示编号 i 停留在节点 j 的最短距离
for(int i = 1; i <= n; i++){
for(int u = 0; u <= idx; u++){
if(dp[i][u] >= INF) continue;
for(int k = i + 1; k <= n; k++){ // 只能走编号更大的点。
int v = tr[u][k];
if(st[v]) continue;
dp[k][v] = min(dp[k][v], dp[i][u] + get(i - 1, k - 1));
}
}
}
double ans = INF;
for(int i = 0; i <= idx; i++)
ans = min(ans, dp[n][i]);
if(ans >= INF) cout << "Can not be reached!\n";
else cout << fixed << setprecision(2) << ans << endl;
}
return 0;
}
AC自动机 + 数值进制状压DP
题意
给出 \(n\) 个字典串,一个长度为 \(m\) 目标串,问把目标串重新排位最多能产生多少个字典串,可以重叠且所有串只包含A C G T。目标串长度不超过 \(40\)
数据范围
\(1\leq n \leq 50\)
\(1\leq m \leq 40\)
\(1\leq 字典串长度 \leq 10\)
思路
- 问题转化:使用与目标串字符数量种类相同的字典串重新排列最多能包含多少个字典串,明显是个多模式匹配问题考虑 AC自动机
- 观察数据范围,自动机节点个数最多为 \(500\),目标串长度为 \(40\),每种字符最多 \(40\) 个。
- 考虑状压DP,压缩方式
Hash[i][j][k][l]
表示四种字符分别有 i,j,k,l 种。对于所有的字符情况,状态数最多不超过 \(12000-15000\)。 - 最后先枚举状态再枚举节点,以各个状态为阶段进行DP。状态表示方式
f[i][j] 节点i在状态j的最大值
,然后根据子节点是否有贡献来进行转移即可
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int M = 510, S = 4;
int cnt[M], tr[M][S], idx, fail[M], Hash[45][45][45][45]; // Hash[i][j][k][l] 使用数值进位来状压
int dp[M][15010];
int get(char c){
if(c == 'A') return 0;
if(c == 'G') return 1;
if(c == 'C') return 2;
return 3;
}
void insert(string s){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = get(s[i]);
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
cnt[p]++;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front();
q.pop();
cnt[u] += cnt[fail[u]];
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
int ct[4], num[4];
void init(){
idx = 0;
memset(tr, 0, sizeof tr);
memset(fail, 0, sizeof fail);
memset(cnt, 0, sizeof cnt);
memset(ct, 0, sizeof ct);
memset(num, 0, sizeof num);
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 0;
int n;
while(cin >> n, n){
init();
for(int i = 0; i < n; i++){
string s;
cin >> s;
insert(s);
}
build();
string s;
cin >> s;
for(int i = 0; i < s.size(); i++){
int c = get(s[i]);
ct[c]++;
}
int x = -1;
for(int i = 0; i <= ct[0]; i++){
for(int j = 0; j <= ct[1]; j++)
for(int k = 0; k <= ct[2]; k++)
for(int l = 0; l <= ct[3]; l++)
Hash[i][j][k][l] = ++x;
}
for(int i = 0; i <= idx ;i++)
for(int j = 0; j <= x; j++)
dp[i][j] = -1;
dp[0][0] = 0;
for(num[0] = 0; num[0] <= ct[0]; num[0]++)
for(num[1] = 0; num[1] <= ct[1]; num[1]++)
for(num[2] = 0; num[2] <= ct[2]; num[2]++)
for(num[3] = 0; num[3] <= ct[3]; num[3]++){ // 一定是先循环状态,再循环节点。
int old = Hash[num[0]][num[1]][num[2]][num[3]];
for(int i = 0; i <= idx; i++){
if(dp[i][old] == -1) continue;
for(int k = 0; k < 4; k++){
if(num[k] == ct[k]) continue;
int v = tr[i][k];
num[k]++;
int nxt = Hash[num[0]][num[1]][num[2]][num[3]];
dp[v][nxt] = max(dp[v][nxt], dp[i][old] + cnt[v]);
num[k]--;
}
}
}
int ans = 0;
for(int i = 0; i <= idx; i++)
ans = max(ans, dp[i][x]);
cout << "Case " << ++T << ": ";
cout << ans << endl;
}
return 0;
}
AC自动机 + BFS最短路 + 状压DP(TSP问题)
题意
给出 \(n\) 个资源串,\(m\) 个病毒串,将资源串拼接成一个串,必须包含所有的资源串,
可以重叠,但是不能包含病毒串,问最小的长度为多少?
数据范围
\(1\leq n \leq 10\)
\(1\leq m \leq 1000\)
Each resource is at most \(1000\) characters long.
The total length of all virus codes is at most \(50000\)
思路
- 多模式匹配问题考虑AC自动机,观察 \(n\) 的范围较小,可以考虑状压DP实现。
- 在AC自动机中记录每个资源串的结尾节点,问题等价于不走过病毒串结点,走完所有资源串结尾节点的最短路径长度,其实是一个TSP问题
- 考虑如何表示状态
- 我们发现虽然资源串只有 \(10\) 个不到,但是病毒串数量很大,如果像上面的题那样必然 MLE。考虑优化
- 对于我们真正有用的其实只有资源串的结尾,然而由于AC自动机自身的特性,一个资源串能够标记的节点不止一个,这时候我们应该怎么办呢?
- 事实是,对于每个资源串真正有用的只有他本身自己的结点,贪心的来看,如果由于fail指针传递,其他点有这个资源串状态的距离必然比其大。
- 或者你又会觉得如果一个资源串是另一个资源串的子串的时候,应该怎么看呢?答案是预处理出所有不是其他串子串的资源串。这点非常的重要,也是csdn上大部分题解都没有关心的点,然后会导致代码被hack(原题数据过弱)
- 那么我们现在就拿到了不超过 10 个的资源串结点,这个时候就可以开开心心套 TSP 模板了!(注意处理 TSP 起点的状态)
- 最后,给出状态表示和转移:
- 状态表示:
f[i][j]
表示状态为 \(i\) 时,停留在 \(j\) 资源串结尾点的最短路径长度。 - 状态转移:枚举状态、枚举起点、枚举终点,判断是不是病毒串结点,然后按照 TSP 问题的一般形式转移即可。
- 状态表示:
- 时间复杂度: \(O(m + 2^n * n^2)\)
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
#define Memset(x) memset(x, 0, sizeof x)
using namespace std;
const int M = 60010, S = 2, N = 15;
int tr[M][S], idx, fail[M], ed[N];
int g[N][N], n, m, dp[1 << 11][N];
bool st[M], fl[N];
void insert(string s, int id, int t){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i] - '0';
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
if(t)
ed[id] = p;
else
st[p] = true;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front);
q.pop();
if(st[fail[u]]) st[u] = true;
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void init(){
idx = 0;
Memset(fail);
Memset(tr);
Memset(st);
memset(dp, 0x3f, sizeof dp);
memset(g, 0x3f, sizeof g);
}
int dist[M];
void bfs(int start, int id){
queue<int> q;
memset(dist, 0x3f, sizeof dist);
dist[start] = 0;
q.push(start);
while(q.size()){
auto u = q.front();
q.pop();
for(int k = 0; k < S; k++){
int v = tr[u][k];
if(st[v] || dist[v] != 0x3f3f3f3f) continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
for(int i = 0; i < n; i++)
g[id][i] = dist[ed[i]];
}
bool cmp(string a, string b){
return a.size() > b.size();
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
while(cin >> n >> m, n || m){
init();
vector<string> v, str;
for(int i = 0; i < n; i++){
string s;
cin >> s;
v.pb(s);
}
memset(fl, false , sizeof fl);
sort(v.begin(), v.end(), cmp); // 处理资源串是其他资源串的情况
for(int i = 0; i < v.size(); i++){
for(int j = i + 1; j < v.size();j ++){
if(v[i].find(v[j]) != -1)
fl[j] = true;
}
}
int tot = 0;
for(int i = 0; i < v.size(); i++)
if(!fl[i]){
insert(v[i], tot++, 1);
str.pb(v[i]);
}
n = tot;
for(int i = 0; i < m; i++){
string s;
cin >> s;
insert(s, 0, 0);
}
build();
for(int i = 0; i < n; i++){
dp[1 << i][i] = str[i].size(); // 根节点到资源点最短距离一定是其长度
bfs(ed[i], i);
}
// 做一次 TSP 状压问题
for(int i = 0; i < 1 << n; i++)
for(int j = 0; j < n; j++)
if(i >> j & 1){
for(int k = 0; k < n; k++){
if(!(i >> k & 1) && g[j][k] != 0x3f3f3f3f)
dp[i ^ (1 << k)][k] = min(dp[i ^ (1 << k)][k], dp[i][j] + g[j][k]);
}
}
int ans = 0x3f3f3f3f;
for(int i = 0; i < n; i++)
ans = min(ans, dp[(1 << n) - 1][i]);
cout << ans << endl;
}
return 0;
}(
`