字符串题单
首先感谢 Democy 爷愿意帮我调翔山代码,真的超级谢谢!我真的想不到还会有第二个人会来不厌其烦的解答我的笨比小问题和帮我看翔山代码,再次谢谢!Thanks♪(・ω・)ノ
P1381
看到这道题,首先一个想法是双指针对吧,双指针的关键在于加入和删除的操作怎么维护。总而言之我们总是要吧 \([l, r]\) 里面的单词放到一个集合里面去,然后求这个集合里面有多少单词我们要背。如何维护?我们可以开一个桶 \(M(str)\) 代表 \(str\) 在集合里面出现了几次。\(tot\) 为集合里面有多少个我们要背的单词。
双指针会先拓展一个 \(r\) 增大区间,那么这个时候 \(M(str[r])+1\),如果恰好 \(M(str[r]) = 1\) 而且 \(str[r]\) 要背,说明新的加入了 \(tot+1\)。
然后会增大 \(l\) 缩小区间,那么这个时候 \(M(str[l]) - 1\),如果恰好 \(M(str[l]) = 0\) 而且 \(str[l]\) 要背,那么 \(tot - 1\)。
双指针还要考虑啥时候停止。啥时候停止呢?就是缩小也不可能让我们的答案更优,也就是说如果我们 \(tot\) 减小(就是 \(M(str[l]) = 0\& str[l] 要背\))那么我们就停止减小 \(l\)。这个时候第一个要满足的最优条件是满足不了的。
当然,\(M\) 里面用字符串哈希
//SIXIANG
#include <iostream>
#include <string>
#include <map>
#define MAXN 100000
#define ll __int128
#define QWQ cout << "QWQ" << endl;
using namespace std;
int m, n;
const ll Mod = 998244353, base = 137;
string s1[MAXN + 10], s2[MAXN + 10];
map <ll, ll> M1, M2;
ll hash1[MAXN + 10], hash2[MAXN + 10];
ll gethash(string str) {
ll x = 0;
for(int p = 0; p < str.size(); p++)
x = (x * base + (str[p] - 'a' + 1)) % Mod;
return x;
}
void init() {
cin >> m;
for(int p = 1; p <= m; p++) {
cin >> s1[p];
if(M1[gethash(s1[p])]) continue;
hash1[p] = gethash(s1[p]);
M1[hash1[p]] = 1;
}
cin >> n;
for(int p = 1; p <= n; p++) cin >> s2[p], hash2[p] = gethash(s2[p]);
int l = 1, r = 0, tot = 0, tmp = 0, ans1 = 0, ans = 0x3f3f3f3f;
for(int r = 1; r <= n; r++) {
M2[hash2[r]]++;
if(M2[hash2[r]] == 1 && M1[hash2[r]]) tmp++;
for(; l <= r; l++) {
if(tmp > tot) {
tot = tmp;
ans = r - l + 1;
}
else if(tmp == tot) ans = min(ans, r- l + 1);
if(!M1[hash2[l]]) continue;
if(M2[hash2[l]] == 1 && M1[hash2[l]]) break;
M2[hash2[l]]--;
}
}
cout << tot << endl << ((!tot) ? (0) : ans) << endl;
}
int main() {
init();
}
P4391
我之前详细讲过 kmp 里面就有这个的说
P4421
人话版题意:有 \(n\) 个字符串,有多少组数对 \((i, j)\) 满足 \(str_i\) 是 \(str_j\) 的子串呢?
\(n\le 2e4\),不能硬来,字符串长度小于等于 \(10\),可以硬来。
我们可以先规定一下 \(i < j\),然后再把字符串反过来考虑 \(i > j\) 的情况。下面我们强行规定一下 \(i < j\)。
字符串长度小于等于 \(10\),不直接枚举子串都对不起它(划掉),然后可以干啥呢。对于字符串 \(str_i\),我们可以枚举所有子串,放到一个 set 里去重(有重复的只算一个),然后找 \(str_{1\sim i - 1}\) 有多少这样的串的,这个东西可以用一个 map。但是由于 Democy 爷云,map 在存长度为 10 这样的字符串时慢的要死,所以我们可以用哈希。
代码
//SIXIANG
#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <set>
#define MAXN 200000
#define ll __int128
#define QWQ cout << "QWQ" << endl;
using namespace std;
const ll Mod = 1000014514191981037, base = 27;
string str[MAXN + 10];
ll hash1[MAXN + 10];
map <ll, ll> M;
ll get_hash(string str) {
ll x = 0;
for(int p = 0; p < str.size(); p++)
x = (x * base + (ll)(str[p] - 'a' + 1)) % Mod;
return x;
}
void init() {
int n; cin >> n;
for(int p = 1; p <= n; p++) {
cin >> str[p];
hash1[p] = get_hash(str[p]);
}
set <ll> sx;
ll ans = 0;
for(int p = 1; p <= n; p++) {
sx.clear();
for(int i = 0; i < str[p].size(); i++) {
ll x = 0;
for(int j = i; j < str[p].size(); j++)
x = (x * base + (ll)(str[p][j] - 'a' + 1)) % Mod, sx.insert(x);
}
for(set<ll>::iterator it = sx.begin(); it != sx.end(); it++)
ans += M[(*it)];
M[hash1[p]]++;
}
M.clear();
for(int p = n; p >= 1; p--) {
sx.clear();
for(int i = 0; i < str[p].size(); i++) {
ll x = 0;
for(int j = i; j < str[p].size(); j++)
x = (x * base + (ll)(str[p][j] - 'a' + 1)) % Mod, sx.insert(x);
}
for(set<ll>::iterator it = sx.begin(); it != sx.end(); it++)
ans += M[(*it)];
M[hash1[p]]++;
}
int rest[50], top = 0;
while(ans) rest[++top] = int(ans % 10), ans /= 10;
for(int p = top; p >= 1; p--) cout << rest[p];
}
int main() {
init();
}
P7469
本来看到这道题 NOIOtg 组想袜这个提高组太水了吧我一定能拿 250+,然后仔细一看我超一绿两黑我啥都没说呜呜呜呜呜所以这道题真的就签到涨信心的吗 QAQ
注意到这样一件事情,bob 最后剩下的是原串的子串,而 Alice 是子序列。子串比子序列更好确定,而且 \(n\le 3000\),我们很想枚举 \(t\) 的子串。枚举完子串就是判断它能否是 \(s\) 的一个子序列了。这好办,我们拿一个指针扫一下就行了。
我们很容易得到这样一个算法,对于 bob 的 \(t\) 首先枚举一个 \(l\),不断扩大 \(r\)。有一个指针 \(pos\) 在 Alice 的 \(s\) 上不断右移,对于每一个新来的 \(t_r\),看看能不能右移 \(pos\) 使得 \(s_{pos}\) 等于 \(t_r\),可以就可以,把它丢到一个 set 里面去重,不可以的话就直接结束,再扩大 \(r\) 不会得到合法串。
但是这道题很恶心的一点在于,set 去重会 TLE,所以得哈希一下,然后把它排序去重。
卡这点常数至于吗呜呜呜大常数人自闭
//SIXIANG
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#define MAXN 9000000
#define ll __int128
#define QWQ cout << "QWQ" << endl;
using namespace std;
const ll Mod = 1000014514191981037, base = 137;
ll tot = 0, a[MAXN + 10];
void init() {
int n; cin >> n;
string s1, s2;
cin >> s1 >> s2;
int cnt = 0;
for(int l = 0; l < s2.size(); l++) {
int q = 0;
string tmp = "";
ll x = 0;
for(int r = l; r < s2.size(); r++) {
tmp += s2[r];
x = (x * base + (s2[r] - 'a' + 1)) % Mod;
while(s1[q] != s2[r] && q < s2.size()) q++;
if(s1[q] != s2[r]) break;
else q++, a[++tot] = x;
}
}
sort(a + 1, a + tot + 1);
int len = unique(a + 1, a + tot + 1) - a - 1;
cout << len << endl;
}
int main() {
init();
}
P3879
相当模板的一道 trie 树题。对于每一篇文章的所有单词插入到一棵 trie 里面,在每一个点的结尾记录一下这个单词在那些文章出现过,可以用 set,也可以就直接生猛的对于每一个结尾 \(now\),开一个 \(vis(i,now)\) 记录以 \(now\) 结尾的这个字符串在 \(i\) 文章内是否出现过。因为 \(n\le 1e3\),所以这并不会有事。
代码是很久之前写的,不是很理解之前为啥要封装一个 trie 的说,有点离谱(
#include<iostream>
#include<cstring>
#include<string>
using namespace std;
int n,m;
struct Trie{
int T[500010][26],siz;
bool vis[1010][500010];
void clear()
{
siz=0;
memset(T,0,sizeof(T));
memset(vis,0,sizeof(vis));
}
void insert(string str,int index)
{
int now=0;
for(int p=0;p<str.size();p++)
{
int id=int(str[p]-'a');
if(!T[now][id])
T[now][id]=++siz;
now=T[now][id];
}
vis[index][now]=1;
}
void ask(string str)
{
int now=0;
for(int p=0;p<str.size();p++)
{
int id=int(str[p]-'a');
if(!T[now][id])
{
cout<<endl;
return ;
}
now=T[now][id];
}
for(int p=1;p<=n;p++)
if(vis[p][now])
cout<<p<<' ';
cout<<endl;
}
}Tr;
int main()
{
string in;
cin>>n;
for(int p=1,x;p<=n;p++)
{
cin>>x;
for(int i=1;i<=x;i++)
cin>>in,Tr.insert(in,p);
}
cin>>m;
for(int p=1;p<=m;p++)
{
cin>>in;
Tr.ask(in);
}
}
P6286
在某谷上写了详细题解的说 Link
P3082
这份题单最不太会的一道题,虽然好像不难的样子,我要酝酿酝酿再来写题解(大雾
P7114
不难诶 awa
首先我们会发现 \(F\) 函数的限制相当莫名其妙,我们不管这个,我们来管管没有这个限制的东西
给定一个长度为 \(n\) 的字符串 \(str\),求有多少种方式能够使得它可以拆成 \((AB)^iC\) 的形式。\(n\le 2^{30}\)。
AB 是一个感情很好的整体,我们不要拆散它们,我们可以记它是 \(S\),求 \((S^i)C\) 的拆分数,很显然第一步是先枚举 \(S\) 这个前缀,很显然 \(i\le \lfloor \dfrac{n}{|S|}\rfloor\)。那么我们就可以在一个调和级数的时间内枚举完每个 \(S\) 和 \(i\)。这个时候我们的时间就是 \(O(n\log_2 n)\) 的,下面我们要在 \(O(1)\) 的范围内判断完长度 \(|S|\times i\) 的前缀是不是 \(S\) 重复 \(i\) 次所构成的,还记得我们的 KMP 吗,我们就可以用 KMP。然后这个问题就解决了。
下面我们加上 \(F\) 函数的限制,我们的问题就变成了
已知 \(S = AB\),求 \(S\) 有多少前缀 \(A\) 满足 \(F(A)\le F(C)\)
这个约束很好做,\(F(C)\) 可以维护一个后缀 \(F\)。\(F(A)\) 可以维护一个前缀 \(F\)。\(F(C)\) 已知。考虑到 \(F(A)\) 值域只有 26,完全可以开一个桶暴力统计每个值域。但是这样你会无情的 TLE 掉因为这个时候你的时间复杂度是 \(O(26(n\log_2n))\) 的 会多看一眼爆炸掉。
我们这个桶是干嘛的?要支持什么?要支持单点修改和前缀查询。修改有多少次?修改只有 \(n\) 次。查询有多少次?查询有 \(n\log_2 n\) 次。所以我们可以维护前缀和,这样我们修改复杂度就是 \(O(26)\) 但是我们查询就是 \(O(1)\) 的,在 \(O(n\log_2 n)\) 面前,我们这个 \(O(26n)\) 的修改复杂度可以忽略掉。时间复杂度 \(O(n\log_2 n)\)。
//SIXIANG
#include <iostream>
#include <string>
#include <cstring>
#define ll long long
#define MAXN 1048576
#define QWQ cout << "QWQ" << endl;
using namespace std;
int fail[MAXN + 10], len;
int qzf[MAXN + 10], hzf[MAXN + 10];
ll tong[27], qzt[27];
int t[27];
string str;
void prepare() {
fail[1] = 0;
int i;
for(int p = 2; p < str.size(); p++) {
i = fail[p - 1];
while(str[i + 1] != str[p] && i)
i = fail[i];
if(str[i + 1] == str[p]) fail[p] = i + 1;
else fail[p] = i;
}
memset(tong, 0, sizeof(tong));
memset(qzf, 0, sizeof(qzf));
memset(hzf, 0, sizeof(hzf));
memset(t, 0, sizeof(t));
for(int p = 1; p < str.size(); p++) {
t[str[p] - 'a']++;
if(t[str[p] - 'a'] % 2 == 1) qzf[p] = qzf[p - 1] + 1;
else qzf[p] = qzf[p - 1] - 1;
}
memset(t, 0, sizeof(t));
for(int p = str.size() - 1; p >= 1; p--) {
t[str[p] - 'a']++;
if(t[str[p] - 'a'] % 2 == 1) hzf[p] = hzf[p + 1] + 1;
else hzf[p] = hzf[p + 1] - 1;
}
}
ll Count(int p, int i) {
return qzt[hzf[p * i + 1]];
}
void init() {
cin >> str;
len = str.size();
str = " " + str;
prepare();
for(int i = 0; i < 26; i++) tong[i] = 0;
ll ans = 0;
for(int p = 1; p < len; p++) {
if(p != 1) {
for(int i = 2; i <= len / p; i++) {
int r = i * p;
if(r == len) continue;
if(r % (r - fail[r]) == 0 && p % (r - fail[r]) == 0)
ans += Count(p, i);
}
}
if(p != 1) ans += Count(p, 1);
tong[qzf[p]]++;
qzt[0] = tong[0];
for(int i = 1; i <= 25; i++)
qzt[i] = qzt[i - 1] + tong[i];
}
cout << ans << endl;
}
int main() {
int T; cin >> T;
while(T--)
init();
}