大一下第一周学习笔记

新的学期

ACM依然是我的热爱

怀着一颗热爱的心去学习,去训练

 

周一 3.1(KMP+Manacher)

回归编程

我一位很优秀的同学说

如果你做的事情是你热爱的,那么你就不会觉得累

acm就是我热爱的,写题学算法永远不会觉得累

用热爱驱动训练,加油

 

现在的大致规划是

先搞好周训的内容和提高篇那本书的内容

周训学号,那本书上的题基本做完

之后再去洛谷题单刷题

 

【模板】KMP字符串匹配

算是复习了

自己靠原理写出来了,有几个地方注意一下

1.求next数组是b的,不是a的

2.next数组的含义是next[i]表示0~i-1这一个字串中最长的前缀=后缀的长度,注意不包括i

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e6 + 10;
char a[N], b[N];
int Next[N], lena, lenb;

void get_next()
{
	Next[0] = -1;
	int i = 0, j = -1; //注意一开始i为0,j为-1 为了让Next[1] = 0 也不会存在全串的情况 
	while(i < lenb)
	{
		if(j == -1 || b[i] == b[j]) //是配b数组 
		{
			i++; j++;
			Next[i] = j;
		}
		else j = Next[j];
	}
}

void kmp()
{
	int i = 0, j = 0;
	while(i < lena)
	{
		if(j == -1 || a[i] == b[j])
		{
			i++; j++;
			if(j == lenb) printf("%d\n", i - j + 1);//i-j就是匹配的初始位置,是0开始算的 
		}                                           //=lenb后会重新匹配的,因为j越界了,不可能相等 
		else j = Next[j];
	}
}

int main()
{
	scanf("%s%s", a, b);
	lena = strlen(a), lenb = strlen(b);
	get_next();
	kmp();
	REP(i, 0, lenb) printf("%d ", Next[i + 1]); //Next数组的含义 
	puts("");
	
	return 0;
}

P2375 [NOI2014] 动物园(kmp原理拓展)

独立做出蓝题

这题是道好题目

Next[i]可以理解为前i个字符,理解为下标也可以

首先要求前后缀相同而不重复的最长长度

我这里是求了两遍kmp

先正常的,第二遍加一个判断求出另外一个数组。直接暴力会超时,这样不超时

然后统计的时候记忆化一下防止超时就好了

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e6 + 10;
const int MOD = 1e9 + 7;
int Next[N], f[N], k[N], len;
char a[N];

void get_next1()
{
	Next[0] = -1;
	int i = 0, j = -1;
	while(i < len)
	{
		if(j == -1 || a[i] == a[j])
		{
			i++; j++;
			Next[i] = j;	
		} 
		else j = Next[j];
	}
}

void get_next2()
{
	Next[0] = -1;
	int i = 0, j = -1;
	while(i < len)
	{
		if((j == -1 || a[i] == a[j]) && (j + 1) * 2 <= i + 1)
		{
			i++; j++;
			k[i] = j;	
		} 
		else j = Next[j];
	}
}

int num(int i)
{
	if(f[i]) return f[i];
	if(i <= 0) return 0;
	return f[i] = 1 + num(Next[i]);
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%s", a);
		len = strlen(a);
		get_next1();
		get_next2();
		
		int ans = 1;
		memset(f, 0, sizeof(f));
		_for(i, 1, len) 
			ans = ans * 1ll * (num(k[i]) + 1) % MOD;
		printf("%d\n", ans);
	}
	
	return 0;
}

 

P3805 【模板】manacher算法

同样理解算法原理

求一个字符串最大的回文子串长度

暴力O(n^2)这个算法可以优化到O(n)

和kmp有点像,都是利用之前已经匹配过的信息

首先我们需要一个对称中心,因为偶回文的对称中心是空隙,所以要全部转化为奇回文

于是加入#,同时两端加入不一样的字符防止越界,这就是初始化

初始化用string会好写一些

用p[i]表示以i为对称中心的最长回文串半径

我这个写法是不包括i本身的半径

对于之前求过的回文串最长右边界为mx

如果新的对称中心在mx左侧,就可以利用前面的信息确定一部分的回文串

这时注意不能超过mx-i,所以要取min

也就是利用前面的信息赋个初值,然后开始暴力判断,优化在这个赋初值这里

判断后我们要尽可能拓展mx,是的后面的i尽可能能赋初值

然后p[i]就是原串中以i这个位置为对称中心的的回文串长度,用它来更新ans

注意p数组要开字符串长度的两倍,因为字符串会初始化

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1.1e7 + 10;
string a, str;
int p[N << 1], len; 

int Manacher()
{
	int id = 0, mx = 0, ans = 0;
	len = str.size();
	REP(i, 1, len)
	{
		if(i < mx) p[i] = min(p[2 * id - i], mx - i); //赋初值 
		else p[i] = 0;
		while(str[i - p[i] - 1] == str[i + p[i] + 1]) p[i]++; //暴力拓展 
		if(i + p[i] > mx) //尽可能拓展mx 
		{
			mx = i + p[i];
			id = i;
		}
		ans = max(ans, p[i]);  //用p[i]更新答案 
	}	
	return ans;
} 

int main()
{
	ios::sync_with_stdio(0); //加速cin,写了之后要不全部用cout,要不全部用printf 
	cin.tie(0);
	
	cin >> a;
	len = a.size();
	str = "&"; //str为初始化后的结果 
	REP(i, 0, len) str += "#", str += a[i];
	str += "#^"; 
	cout << Manacher() << endl;
	
	return 0;
}

P1659 [国家集训队]拉拉队排练(回文串问题)

用Manacher算法求出以每个点为对称中心的回文串,可以说它是一个工具

然后统计答案就行,因为Manacher算的是最长回文串,对于p[i]来说,p[i] - 2, p[i] - 4都是答案

for一遍就好,注意要用快速幂

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e6 + 10;
const int MOD = 19930726;
string a, str;
int p[N << 1], cnt[N], n; 
long long k;

int mul(int a, int b) { return 1ll * a * b % MOD; }

void Manacher()
{
	int id = 0, mx = 0, len = str.size();
	REP(i, 1, len)
	{
		if(i < mx) p[i] = min(mx - i, p[2 * id - i]);
		else p[i] = 0;
		while(str[i - p[i] - 1] == str[i + p[i] + 1]) p[i]++;
		if(mx < i + p[i])
		{
			mx = i + p[i];
			id = i;
		}
		if(p[i] & 1) cnt[p[i]]++;
	}
}

int binpow(int a, int b)
{
	int res = 1;
	for(; b; b >>= 1)
	{
		if(b & 1) res = mul(res, a);
		a = mul(a, a);	
	}
	return res;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> n >> k >> a;
	str = "%";
	REP(i, 0, n) str += "#", str += a[i];
	str += "#@";
	
	Manacher();
	int ans = 1, t = 0;
	for(int i = n; i >= 1; i--)
		if(i & 1)
		{
			if(cnt[i] + t <= k) 
			{
				ans = mul(ans, binpow(i, cnt[i] + t));
				k -= cnt[i] + t;
				t += cnt[i];
			}
			else
			{
				ans = mul(ans, binpow(i, k));
				k = 0;
				break;
			}
		}
	if(k) puts("-1");
	else printf("%d\n", ans);

	return 0;
}

周二 3.2(Manacher+Tire树+AC自动机)

P4555 [国家集训队]最长双回文串(Manacher+堆)

这题写了一上午,一点一点想出,很爽

写完看了题解发现我的做法和别人的都不一样哈哈哈

首先用Manacher算法求出p[i],然后再每一个回文子串的左右端点标记一下,也就是代码中的L数组和R数组

很容易想到标记时长度更长肯定更优秀,所以我取了max

然后我就想到了一个O(n^2)的算法,枚举一个回文子串的右端点,再这个点前面枚举左端点

那么长度就是两个回文子串长度和-2 * 两个回文子串公共的部分,这个拿笔画一下就好了

那么显然要优化,不然会超时

式子len1 + len2 - 2 * 公共部分

len1是固定的,主要是后面两部分要最大

我发现当i++时,只有公共部分+2,而且是所有的都加2

所有都加2表示不同的len2它们的相对大小位置是不变的

我们要最优,那就要最大的

那么我们就可以用一个优先队列来维护

这里提一下,优先队列是大根堆,sort是从小到大。我以为优先队列小根堆卡了半天

用堆维护len2,后加入的要加上2,因为公共部分变了

这样就用一个堆优化成了nlogn了,主要是观察那个式子的特性

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10;
string a, str;
int p[N << 1], L[N << 1], R[N << 1], len;

struct node 
{ 
	int j, l;
	bool operator < (const node& rhs) const
	{
		return l < rhs.l;
	}
};
priority_queue<node> q;

void Manacher()
{
	int id = 0, mx = 0; len = str.size();
	REP(i, 1, len)
	{
		if(i < mx) p[i] = min(mx - i, p[2 * id - i]);
		else p[i] = 0;
		while(str[i + p[i] + 1] == str[i - p[i] - 1]) p[i]++;
		if(i + p[i] > mx)
		{
			mx = i + p[i];
			id = i;
		}
		R[i + p[i]] = max(R[i + p[i]], p[i] * 2 + 1);
		L[i - p[i]] = max(L[i - p[i]], p[i] * 2 + 1);
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> a;
	len = a.size();
	str = "@";
	REP(i, 0, len) str += "#", str += a[i];
	str += "#!";
	
	Manacher();
	int ans = 0, t = 4;
	q.push(node{1, L[1]});
	q.push(node{2, L[2] + 2});
	_for(i, 2, len - 2)
	{
		ans = max(ans, (R[i] - 1) / 2 - 1);
		if(i + 1 <= len - 2) q.push(node{i + 1, L[i + 1] + t});
		t += 2;
		while(!q.empty())
		{
			int j = q.top().j;
			if(j + (L[j] - 1) / 2 > i) break;
			q.pop();
		}
		if(!q.empty())
		{
			int j = q.top().j;
			ans = max(ans, (R[i] + L[j] - 2 * (i - j + 1)) / 2);
		}
	}
	printf("%d\n", ans);

	return 0;
}

复习nth_element

nth_element(a, a + x, a + n) 

可以把第x大(第0大, 第1大)的元素放在a[x]

x前面的数都小于等于x,后面都大于等于x

可以将排序好后a[x]的值放在a[x],前面小于等于a[x]后面大于等于a[x]

 

Immediate Decodability(Trie树裸题)

做了一道裸题复习了一下Trie树

顺利写出,果然懂原理还是非常重要

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1100;
int t[N][2], vis[N], cnt, ans;
char s[15];

void add(char* str)
{
	int len = strlen(str), p = 0;
	REP(i, 0, len)
	{
		int id = str[i] - '0';
		if(i == len - 1 && t[p][id]) ans = 1;
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
		if(vis[p]) ans = 1;
	}
	vis[p] = 1;
}

int main()
{
	int kase = 0;
	while(~scanf("%s", s))
	{
		memset(t, 0, sizeof(t));
		memset(vis, 0, sizeof(vis));
		cnt = ans = 0;
		
		add(s);
		while(scanf("%s", s) && strcmp(s, "9")) add(s);
		if(ans) printf("Set %d is not immediately decodable\n", ++kase);
		else printf("Set %d is immediately decodable\n", ++kase);
	}

	return 0;
}

P3808 【模板】AC自动机(简单版)

通过好几个小时的研究

最后静下心来慢慢理解慢慢读,终于理解了AC自动机,不容易

但是理解还不深刻,需要做题来强化

实际上就是Trie树上跑kmp

kmp中是j = next[j],是一个串中跳

但是这里的转移就是所有串中最长的next,所以会在多个串中跳来跳去

kmp中一个串会匹配多次,AC自动机也一样,所以要避免重复计算

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e6 + 10;
int t[N][26], fail[N], End[N], n, cnt = 1; //cnt = 1写在全局,不要写到add 
char s[N];

void add(char* str)
{
	int p = 1, len = strlen(str); //p从1开始 
	REP(i, 0, len)
	{
		int id = str[i] - 'a';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
	}
	End[p]++;
}

void get_fail() //AC自动机主要过程。相当于把Trie树补全,后面查询时直接查 
{
	_for(i, 0, 25) t[0][i] = 1; //注意有0节点,因为涉及到父亲的fail 
	fail[1] = 0;           
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])  
			{
				q.push(t[u][i]); //有则加入 
				int v = fail[u];
				while(v && !t[v][i]) v = fail[v]; //类似求next数组中不匹配就一直J = next[j] 
				fail[t[u][i]] = t[v][i]; //fail[t[u][i]] 
			}
			else t[u][i] = t[fail[u]][i]; //值肯定存在,因为更浅的Trie都补全了 
		}                  //如果失配了,那么就跳到t[fail[u]][i]
	}               
}

int main()
{
	scanf("%d", &n);
	_for(i, 1, n)
	{
		scanf("%s", s);
		add(s);
	}
	
	get_fail();
	scanf("%s", s);
	int len = strlen(s), p = 1, ans = 0;
	REP(i, 0, len) //Trie补全后直接遍历就好 
	{
		int id = s[i] - 'a';
		p = t[p][id];
		ans += End[p]; 
		End[p] = 0; //避免重复计算,因为回跳可能像kmp跳回自己,后面又算一次 
	}
	printf("%d\n", ans);

	return 0;
}

周三 3.3 (AC自动机)

P3808 【模板】AC自动机(简单版)

发现昨天写的那个模板有问题

需要把所有后缀都统计一遍,同时防止重复

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)  
using namespace std;

const int N = 1e6 + 10;
int t[N][26], fail[N], End[N], n, len, cnt = 1; 
char s[N];

void add(char* str)
{
	int p = 1; len = strlen(str);
	REP(i, 0, len)
	{
		int id = str[i] - 'a';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];	
	} 
	End[p]++; //注意重复单词 
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];	//t[fail[u]][i]不可能为0,一定指向正确的节点 
			}
			else t[u][i] = t[fail[u]][i];	//t[fail[u]][i]一定是指向一个确定的节点 
		}                                   //补全Tried树 
	} 
}

int main()
{
	scanf("%d", &n);
	_for(i, 1, n)
	{
		scanf("%s", s);
		add(s);
	}
		
	get_fail();
	scanf("%s", s);
	int p = 1, ans = 0; len = strlen(s);
	REP(i, 0, len)
	{
		p = t[p][s[i] - 'a'];
		for(int j = p; j > 1 && End[j] != -1; j = fail[j]) //遍历所有后缀 
		{
			ans += End[j];
			End[j] = -1;	
		} 
	} 
	printf("%d\n", ans);
	
	return 0;
}

Hdu 2222(AC自动机裸题)

End数组写++不要写=1

觉得题目没说清楚会不会有重复的单词,重复了怎么算

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)  
using namespace std;

const int N = 5e5 + 10;
const int M = 1e6 + 10;
int t[N][26], fail[N], End[N], n, len, cnt; 
char s[M];

void add(char* str)
{
	int p = 1; len = strlen(str);
	REP(i, 0, len)
	{
		int id = str[i] - 'a';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];	
	} 
	End[p]++;  
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
			}
			else t[u][i] = t[fail[u]][i];
		}
	}
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		cnt = 1;
		memset(t, 0, sizeof(t));
		memset(fail, 0, sizeof(fail));
		memset(End, 0, sizeof(End));
		
		scanf("%d", &n);
		_for(i, 1, n)
		{
			scanf("%s", s);
			add(s);
		}
		
		get_fail();
		scanf("%s", s);
		int p = 1, ans = 0; len = strlen(s);
		REP(i, 0, len)
		{
			p = t[p][s[i] - 'a'];
			for(int j = p; j > 1 && End[j] != -1; j = fail[j])
			{
				ans += End[j];
				End[j] = -1;
			}
		}
		printf("%d\n", ans);
	}
	
	return 0;
}

bzoj 4327(AC自动机)

用AC自动机可以遍历所有主串与模式串匹配的部分

每个p的位置到根的这个子串就是一个匹配的部分,这个串是主串的一个子串,也是某些模式串的前缀(或整个串)

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)  
using namespace std;

const int N = 4e5 + 10;
const int M = 1e7 + 10;
int t[N][4], fail[N], End[N], fa[N], vis[N], size[N];
int len, cnt, n, m;
char T[M], s[M];
map<char, int> id;

void add(char* str, int i)
{
	int p = 1; len = strlen(str);
	REP(i, 0, len)
	{
		int k = id[str[i]];
		if(!t[p][k]) t[p][k] = ++cnt;
		fa[t[p][k]] = p;
		p = t[p][k];
	}
	End[i] = p;
}

void get_fail()
{
	_for(i, 0, 3) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 3)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
			}
			else t[u][i] = t[fail[u]][i];
		}
	}
}

int main()
{
	cnt = 1;
	id['E'] = 0; id['S'] = 1;
	id['W'] = 2; id['N'] = 3;
	
	scanf("%d%d%s", &n, &m, T);
	_for(i, 1, m)
	{
		scanf("%s", s);
		add(s, i);
		size[i] = strlen(s);
	}
	
	get_fail();
	int p = 1; len = strlen(T);
	vis[1] = 1;
	REP(i, 0, len)
	{
		p = t[p][id[T[i]]];
		for(int j = p; j > 1 && !vis[j]; j = fail[j]) //遍历所有主串和模式串匹配的部分 
		{                                             //注意不要重复遍历 
			vis[j] = 1;
			while(!vis[fa[j]]) j = fa[j], vis[j] = 1;
		}
	}
	
	_for(i, 1, m)
	{
		int t = End[i];
		if(vis[t]) printf("%d\n", size[i]);
		else
		{
			int ans = 0;
			while(!vis[fa[t]]) t = fa[t], ans++;
			printf("%d\n", size[i] - ans - 1);
		}
	}

	return 0;
}

bzoj 3940(AC自动机+栈)

之前做过一道kmp+栈,这里改成多个串就是AC自动机

思路是一样的,只不过实现方法不一样

这里有一点就是会重复遍历,容易超时

所以记忆化一下

不合法要返回-1,不要返回0,和没遍历过混淆

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)  
using namespace std;

const int N = 1e5 + 10;
int t[N][26], fail[N], End[N], k[N], d[N], f[N];
int len, n, cnt = 1;
char T[N], s[N]; 

void add(char* str)
{
	int p = 1, h = 0; len = strlen(str);
	REP(i, 0, len)
	{
		int id = str[i] - 'a';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
		h++;
	}
	End[p] = 1;
	d[p] = h;
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];	
			}	
			else t[u][i] = t[fail[u]][i];
		} 
	}
}

int find(int p)
{
	if(p <= 1) return -1;
	if(f[p]) return f[p];
	if(End[p]) return f[p] = p;
	return f[p] = find(fail[p]);
}

int main()
{
	scanf("%s%d", T, &n);
	_for(i, 1, n)
	{
		scanf("%s", s);
		add(s);
	}
	
	get_fail();
	int p = 1, top = 0; len = strlen(T);
	REP(i, 0, len)
	{
		s[++top] = T[i];
		p = t[p][T[i] - 'a'];
		k[top] = p;
		int t = find(p);
		if(t > 1) top -= d[t], p = k[top];
	}
	_for(i, 1, top) printf("%c", s[i]); puts("");

	return 0;
}

周四 3.4(AC自动机)

今天能抽出的时间稍微少了一些

加上笔记本忘记充电智障了耽误了时间

不过我真的一天不碰键盘就手痒

晨跑习惯了,今天下雨没跑身体就不爽

保持下去

「一本通 2.4 练习 3」单词(AC自动机)

我发现AC自动机好多省选题,基本都是紫题

其实还好啦,算法本身理解比较难,但是理解后做题也就还好吧

这题的话一开始感觉比较奇怪,没有T串

首先建Trie ac自动机是肯定要的,然后咋整呢

我想到把论文当作T串,去匹配。AC自动机可以遍历所有匹配的情况,所以就用一个数组存一下匹配成功的次数

然后做完发现答案错的,原来是单词和单词之间不能连在一起

所以需要一个单词一个单词当作T串取匹配,每次匹配都重新开始

以为大概率超时,然后发现只超时了一个点

想了想,每次其实遍历很快,因为都重新开始了

超时地一个点应该是专门卡的

然后我就优化了一下,就是重复的单词合并在一起,就过了

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e6 + 10;
const int M = 210;
int t[N][26], fail[N], End[M], num[N], v[N]; 
int n, len, cnt = 1;
string s[M];

void add(string s, int j)
{
	int p = 1; len = s.size();
	REP(i, 0, len)
	{
		int id = s[i] - 'a';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
	}
	End[j] = p;
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
			}
			else t[u][i] = t[fail[u]][i];
		}
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> n;
	_for(i, 1, n)
	{
		cin >> s[i];
		add(s[i], i);
	}
	
	get_fail();
	_for(i, 1, n)
	{
		if(v[i] == -1) continue;
		v[i] = 1;
		_for(j, i + 1, n)
			if(s[i] == s[j])
				v[i]++, v[j] = -1;
	}
		
	_for(k, 1, n)
	{
		if(v[k] == -1) continue;
		int p = 1; len = s[k].size();
		REP(i, 0, len)
		{
			p = t[p][s[k][i] - 'a'];
			for(int j = p; j > 1; j = fail[j]) num[j] += v[k];
		}
	}
	_for(i, 1, n) cout << num[End[i]] << endl;
	
	return 0;
}

周六 3.6(AC自动机)

昨天没记录是因为写一道题想了很久没想出来

今天又想了很久,最后看了题解

P2322 [HNOI2006]最短母串问题(AC自动机+状态压缩+bfs求最短路)

这题还是很精彩的

我自己做的时候看到n=12 想到的是搜索,但又有点虚怕会超时

就是搜索枚举所有情况,一些记录注意回溯时恢复现场

最后果然60分超时了

然后就没思路了

看题解发现用到状态压缩的方法

这个知识点我太不熟练了,状压dp我之后会好好搞它一遍

所以做题要独立思考,思考了足够长时间就可以看题解,可能是有些知识点或者算法你不知道

n <= 12也暗示了状态压缩

 

首先很容易想到就是求一条最短路径使得遍历了Trie树上所有的字符串的终点

那么可以把目前经历了哪些字符串用二进制存一个状态

在AC自动机时可以预处理,把当前节点的fail的状态也合并进来

然后就开始bfs,按照字典序拓展,最先达到经历了所有字符串状态的就是答案

bfs的时候记得去掉重复状态,后面重复的状态肯定不是答案,时间优化

然后记录答案的时候可以用ans 和fa数组来存,不用存字符串消耗空间

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 610;
int t[N][26], fail[N], state[N], fa[N * (1 << 12)], vis[N][1 << 12];
int len, n, cnt = 1;
struct node{ int u, s; };
vector<char> ans;
string s;

void add(string str, int i)
{
	int p = 1; len = str.size();
	REP(i, 0, len)
	{
		int id = str[i] - 'A';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
	}
	state[p] |= 1 << (i - 1); 
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
				state[t[u][i]] |= state[fail[t[u][i]]];
			}
			else t[u][i] = t[fail[u]][i];
		}
	}
}

void print(int x)
{
	if(x == 0) return;
	print(fa[x]);
	cout << ans[x];
} 

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> n;
	_for(i, 1, n)
	{
		cin >> s;
		add(s, i);
	}
	
	get_fail();
	queue<node> q;
	q.push(node{1, 0});
	int id = 0;
	ans.push_back(' ');
	
	while(!q.empty())
	{
		node x = q.front(); q.pop();
		int u = x.u, s = x.s;
		
		if(s == (1 << n) - 1)
		{
			print(id);
			break;
		}
		_for(i, 0, 25)
			if(!vis[t[u][i]][s | state[t[u][i]]])
			{
				vis[t[u][i]][s | state[t[u][i]]] = 1;
				q.push(node{t[u][i], s | state[t[u][i]]});
				ans.push_back(i + 'A');
				fa[ans.size() - 1] = id;
			}
		id++;
	}
	
	return 0;
}

周日 3.7 (AC自动机)

昨晚打了第一场积分赛

我太想拿好成绩,从而太紧张了,根本不能静下心来想问题,导致发挥失常

一直犯一些低级错误

我虽然训练了挺久,但是比赛经验还是太少

今晚又有一场

要能静下心来想问题,合理规划时间

比赛时的心理素质也是非常重要的一个方面

「一本通 2.4 练习 5」病毒(AC自动机+dfs+避免重复遍历)

这题显然就是在Trie树上跑,在不经过字符串终点的情况看是否能循环

Trie树上走过一个节点时不仅包含了当前根到此节点的字符串,还包含一些其他字符串,这些字符串是此节点字符串的后缀,也就是fail

一般会在初始化的时候延续fail的某些信息,或者在统计的时候遍历fail

这里是在初始化的时候延续

做dfs就好

需要注意的是,一般题目中dfs不会重复遍历一个节点,但是这道题会,在Trie树上跳可能又跳到同一个节点

所以要判重,不要重复遍历。判重就回溯的时候处理一下就好。

不然会T掉2个点

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 3e4 + 10;
int t[N][2], fail[N], vis[N], End[N];
int len, n, cnt = 1;
string s;

void add(string str)
{
	int p = 1; len = str.size();
	REP(i, 0, len)
	{
		int id = str[i] - '0';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
	}
	End[p] = 1;
}

void get_fail()
{
	_for(i, 0, 1) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 1)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
				End[t[u][i]] |= End[fail[t[u][i]]]; //延续信息 
			}
			else t[u][i] = t[fail[u]][i];
		}
	}
}

bool dfs(int u)
{
	_for(i, 0, 1)
	{
		if(End[t[u][i]] || vis[t[u][i]] == 2) continue;
		if(vis[t[u][i]] == 1) return true;
		vis[t[u][i]] = 1;
		if(dfs(t[u][i])) return true;
		vis[t[u][i]] = 2; //避免重复遍历 
	}
	return false;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> n;
	_for(i, 1, n) cin >> s, add(s);
	
	get_fail();
	if(dfs(1)) puts("TAK");
	else puts("NIE");
	
	return 0;
}

P3796 【模板】AC自动机(加强版)(AC自动机+树形dp)

这题就是要统计每个模式串出现的次数

暴力跳肯定超时,考虑怎么优化

首先那些对答案不产生贡献的点可以略去,所以要修改一下fail数组

其次先标记,然后做一遍树形dp

我自己的做法就是按照深度,从最深的开始,每一个高度的都往它的fail加一次,这样统计是正确的,复杂度也是O(n)

有点像倒序的bfs

本质上应该和树形dp差不多

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 10500 + 10;
int t[N][26], fail[N], End[N], num[N], len, n, cnt = 1; 
string s[160], T;
vector<int> ve[80];

void add(string str, int i)
{
	int p = 1, h = 0; len = str.size();
	REP(i, 0, len)
	{
		h++;
		int id = str[i] - 'a';
		if(!t[p][id]) 
		{
			t[p][id] = ++cnt;
			ve[h].push_back(t[p][id]);
		}
		p = t[p][id];
		
	}
	End[i] = p;
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
		{
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
			}
			else t[u][i] = t[fail[u]][i];
		}
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	while(cin >> n && n)
	{
		cnt = 1;
		memset(t, 0, sizeof(t));
		memset(num, 0, sizeof(num));
		_for(i, 1, 75) ve[i].clear();
		
		_for(i, 1, n) cin >> s[i], add(s[i], i);
		get_fail();
		
		cin >> T;
		len = T.size();
		int p = 1;
		REP(i, 0, len)
		{
			p = t[p][T[i] - 'a'];
			num[p]++;
		}
		
		for(int i = 75; i >= 1; i--)
			for(auto u: ve[i])
				num[fail[u]] += num[u];
		
		int mx = 0;
		_for(i, 1, n)
			mx = max(mx, num[End[i]]);
		cout << mx << endl;
		_for(i, 1, n)
			if(num[End[i]] == mx)
				cout << s[i] << endl;
	}
	
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值