之前一直在打比赛就没时间了。

10.9

CF526D Om Nom and Necklace

题目分析:

这里提供两种思路,感觉都很有用。

KMP

我们令 \(S = AB\),那么显然 \(S\) 必然占据整数个循环节,因为如果不是就可以由此导出矛盾,可以自己画图看看。
那么我们求出循环节之后,如果有 \(tot\) 个循环节,循环节长度为 \(len\),那么也就意味着每一个 \(S\) 都会占据 \(\dfrac{tot}{k}\) 个循环节,那么剩下的 \(tot \% k\) 个循环节以及 \(i \% len\) 的长度是留给最后的 \(A\) 的。
因为这个字符串循环节的存在,所以我们只需要判断 \(|A| \le |S|\) 就可以知道这是否满足要求。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e7+5;
char s[N];
int nxt[N];
int main(){
	int n,k;
	scanf("%d%d",&n,&k);
	scanf("%s",s+1);
	nxt[1] = 0;
	for(int i=2,j=0; i<=n; i++){
		while(j && s[j+1] != s[i])	j = nxt[j];
		if(s[j+1] == s[i])	j ++;
		nxt[i] = j; 
	}
	for(int i=1; i<=n; i++){
		int len = i - nxt[i];
		int tot = i / len; //出现次数
		printf("%d",(tot / k * len) >= (tot % k * len) + (i % len));
		//S 的长度大于等于 A 的长度 
	}
	return 0;
}

Z-function

我们依旧令 \(S = AB\),显然我们可以考虑枚举 \(|S|\),这样依旧可以将序列分为 \(k\) 段,每一段用 \(z\) 数组判断一下长度是否符合条件就好了。
剩下的也就是最后的 \(A\) 了,显然当 \(S\) 固定时符合条件的 \(A\) 是一段区间。
长度非常好求,根据 \(A\)\(S\) 的前缀且 \(|A| \le |S|\) 这两个条件约束一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
char s[N];
int z[N],sum[N];
int main(){
	int n,k;
	scanf("%d%d%s",&n,&k,s+1);
	int l=0,r = 0;
	z[0] = 0,z[1] = n;
	for(int i=2; i<=n; i++){
		if(i <= r)	z[i] = min(r-i,z[i - l + 1]);
		while(i + z[i] <= n && s[i + z[i]] == s[z[i] + 1])	z[i]++;
		if(i + z[i] - 1 > r)	r = i + z[i] - 1,l=i;
	}
	for(int len=1; len<=n/k; len++){
		bool flag = true;
		for(int i=1; i<k; i++){  //判断是否符合条件,SSSSS的条件 
			if(z[len * i + 1] < len){
				flag = false;
				break;
			}
		}
		if(flag){
			sum[len * k] ++;   //这一段区间都是合法的,因为他们的剩余的 A 的长度均不超过 S 
			sum[len * k + min(len,z[len * k + 1]) + 1] --;
		}
	}
	int ans = 0;
	for(int i=1; i<=n; i++){
		ans += sum[i];
		printf("%d",ans > 0);
	}
	return 0;
}

[USACO15FEB] Censoring S

题目分析:

我们可以以样例为例:momooo
当我们匹配完了中间的 moo 时,实际可以理解为我们回到了 mo 的时候并且跳过 moo 与下一个字符 o 进行匹配。
这其实非常像栈的过程,我们就使用栈模拟一下就好了。
栈中记录当前的节点的下标以及匹配的长度,最后留在栈里的就是无法匹配的。

代码:

点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 2e6+5;
char s[N],t[N];
int top = 0,nxt[N];
PII st[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%s%s",s+1,t+1);
	int n = strlen(s + 1);
	int m = strlen(t + 1);
	nxt[1] = 0;
	for(int i=2,j=0; i<=m; i++){
		while(j && t[j + 1] != t[i])	j = nxt[j];
		if(t[j + 1] == t[i])	j++;
		nxt[i] = j;
	}
	for(int i=1,j=0; i<=n; i++){
		while((j && t[j + 1] != s[i]) || j == m)	 j = nxt[j];
		if(t[j + 1] == s[i])	j++;
		st[++top] = {i,j};
		if(j == m){
			top = top - m;
			j = st[top].second;
		}
	}
	for(int i=1; i<=top; i++)	printf("%c",s[st[i].first]);
	return 0;
}

CF1200E Compress Words

题目分析:

我们在 \(KMP\) 求解字符串匹配的时候,实际上就是一直在求较长串 \(i\) 这个前缀与我们较短串的最长公共后缀是什么。
套到这个题里其实发现就是前一个字符串和后一个做匹配,那么直接匹配就好了,匹配结束就合并继续下一个匹配直到结束。
但是我们并不需要从前到后全部扫一遍,因为我们显然只要对最后几个做匹配就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
char s[N],t[N];
int nxt[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int p;
	scanf("%d",&p);
	scanf("%s",s+1);
	int n = strlen(s+1);
	for(int q=1; q<p; q++){
		scanf("%s",t+1);
		int m = strlen(t+1);
		nxt[1] = 0;
		for(int i=2,j=0; i<=m; i++){
			while(j && t[j+1] != t[i])	j = nxt[j];
			if(t[j + 1] == t[i])	j++;
			nxt[i] = j;
		}
		int j = 0;
		for(int i = n - min(n,m) + 1; i<=n; i++){  //s 和 t 取匹配 
			while((j && t[j+1] != s[i]) || j == m)	j = nxt[j];
			if(t[j + 1] == s[i])	j++;
		}
		for(int i = j + 1; i<=m; i++)	s[++n] = t[i];
	}
	for(int i=1; i<=n; i++)	printf("%c",s[i]);
	return 0;
}

[NOIP2020] 字符串匹配

题目分析:

这个题之前一直认为很难,其实比较简单,可能也是因为我的做法比较卡,但也确实可以过。
我们显然可以令 \(S = AB\),那么考虑直接枚举 \(S\) 是多少,以及 \(S\) 出现多少次。
这样就可以知道 \(C\) 是什么,预处理每一个后缀出现次数为奇数的字符数量就可以 \(O(1)\) 得到答案。
我们 \(A\) 的选取其实是任意的,所以预处理出来每一个前缀的奇数字符数小于等于 \(k\) 的前缀的个数就可以 \(O(1)\) 得到总数。
所以总复杂度 \(O(n\log n)\),但是因为我的处理比较垃圾,所以稍微高点。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int cnt,tmp[27][N],z[N],tax[N],pre[N],suf[N],tot[N],rt[N];
char s[N];
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		scanf("%s",s+1);
		//预处理出来每一个后缀出现次数为奇数的字符个数
		int n = strlen(s+1);
		z[0] = 0,z[1] = n;
		int l = 0,r = 0;
		for(int i=2; i<=n; i++){  //求 z 函数,方便判断 SSSS 是否合法 
			if(i <= r)	z[i] = min(r - i,z[i - l + 1]);
			while(i + z[i] <= n && s[i + z[i]] == s[z[i] + 1])	z[i]++;
			if(i + z[i] - 1 > r)	r = i + z[i] - 1,l = i;
		}
		for(int i=n; i>=1; i--){
			suf[i] = suf[i + 1];
			tot[s[i] - 'a'] ^= 1;
			if(tot[s[i] - 'a'] & 1)	suf[i]++;
			else	suf[i]--; 
		}
		for(int i=0; i<=26; i++)	tot[i] = 0;
		for(int i=1; i<=n; i++){
			for(int j=0; j<=26; j++)	tmp[j][i] = tmp[j][i-1];
			pre[i] = pre[i-1];
			tot[s[i] - 'a'] ^= 1;
			if(tot[s[i] - 'a'] & 1)	pre[i]++;
				else	pre[i]--;	
			tmp[pre[i]][i]++;
		}
		for(int j=1; j<=26; j++)
			for(int i=1; i<=n; i++)
				tmp[j][i] += tmp[j-1][i];
		long long ans = 0;
		for(int i=2; i<n; i++){  //因为 AB 非空 
			for(int j=0; (j + 1) * i + 1 <= n; j++){
				if(z[j * i + 1] < i)
					break;
				//这个时候 C 的起始点就是 (j + 1) * i
				ans += tmp[suf[(j + 1) * i + 1]][i-1];
			}
		}
		printf("%lld\n",ans);
		for(int i=0; i<=n + 2; i++)	suf[i] = z[i] = 0;
		for(int i=0; i<=26; i++)	tot[i] = 0;
	}
	return 0;
}

[POI2005]SZA-Template

题目分析:

我们设 \(f[i]\) 表示将前 \(i\) 个字符全部印完的最小的印章长度。
显然 \(f[i]\) 可以等于 \(i\),因为直接拿长度为 \(i\) 的印就可以了,但是还有一种情况,就是 \(f[i] = f[nxt[i]]\)
我们观察样例可以发现:对于一个长度为 \(nxt[i]\) 的前缀和后缀,必定被印章不多余地覆盖。
那么什么时候可以使得 \(f[i] = f[nxt[i]]\) 呢?
我们考虑对于长度为 \(f[nxt[i]]\) 的印章,他一定可以覆盖 \([i - nxt[i] + 1,i]\),那么我们要使得他覆盖 \([1,i]\),那么也就是说长度为 \(f[nxt[i]]\) 的印章可以覆盖 \([1,j]\),其中 \(j \ge i - nxt[i]\),也就是说存在 \(f[j] = f[nxt[i]]\)\(j \ge i - nxt[i]\)
那么就对于每一种印章长度开一个桶,记录他能覆盖的最远点然后转移就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
char s[N];
int tmp[N],nxt[N],f[N];
int main(){
	scanf("%s",s+1);
	int n = strlen(s+1);
	nxt[1] = 0;
	for(int i=2,j=0; i<=n; i++){
		while(j && s[j + 1] != s[i])	j = nxt[j];
		if(s[j + 1] == s[i])	j++;
		nxt[i] = j;
	}
	f[1] = 1,tmp[f[1]] = 1;
	for(int i=2; i<=n; i++){
		f[i] = i;
		if(tmp[f[nxt[i]]] >= i - nxt[i])	f[i] = f[nxt[i]];  
	//f[nxt[i]] 可以覆盖 [i - nxt[i] + 1,i],而 f[j] 可以覆盖 [1,j],所以当 j >= i - nxt[i] 时就可以完全覆盖了 
		tmp[f[i]] = i;   //tmp[i] 其实存的是最后一个 f[x] = i 的 x 的位置,显然 x 越大越好所以可以覆盖之前的位置 
	}
	printf("%d\n",f[n]);
	return 0;
}

CF808G Anthem of Berland

题目分析:

显然我们可以设 \(dp[i][j]\) 表示较长串的前 \(i\) 个现在已经匹配到了较短串的前 \(j\) 个最大的匹配数。
对于转移也很好弄正常来想就好了。
需要注意的是如果匹配结束了,那么其转移到的状态就应该类似 \(KMP\) 去弄。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
const int INF = 1e9+5;
vector<vector<int> > dp;
char s[N],t[N];
int nxt[N];
int main(){
	scanf("%s",s+1);
	scanf("%s",t+1);
	int n = strlen(s + 1);
	int m = strlen(t + 1);
	nxt[1] = 0;
	for(int i=0; i<=n+2; i++){
		vector<int> tmp;
		for(int j=0; j<=m+2; j++){
			tmp.push_back(-INF);
		}
		dp.push_back(tmp);
	}
	for(int i = 2,j = 0; i<=m; i++){
		while(j && t[j + 1] != t[i])	j = nxt[j];
		if(t[j+1] == t[i])	j++;
		nxt[i] = j;
	}
	dp[0][0] = 0;
	for(int i=1; i<=n; i++){
		for(int j=1; j<=m; j++){
			if(s[i] == t[j] || s[i] == '?')
				dp[i][j] = max(dp[i][j],dp[i-1][j-1]);
			if(j == m){
				dp[i][j]++;
				for(int p = nxt[m];p;p=nxt[p])
					dp[i][p] = max(dp[i][p],dp[i][j]);
			}
		}
		for(int j=0; j<=m; j++)
			dp[i][0] = max(dp[i][0],max(dp[i-1][j],dp[i][j]));
	}
	int ans = 0;
	for(int i=0; i<=m; i++)	ans = max(ans,dp[n][i]);
	printf("%d\n",ans);
	return 0;
}

[HNOI2008]GT考试

题目分析:

我们显然可以设 \(dp[i][j]\) 表示前 \(i\) 位现在匹配到了不吉利数字的前 \(j\) 位,且之前没有出现过不吉利数字的总方案数。
转移显然就是枚举这一位填什么。
但是我们会发现:\(n \le 10^9\),也就是正常的转移肯定不行要想办法搞成矩阵的形式。
那么我们就设 \(g[i][j]\) 表示之前匹配到了前 \(i\) 位要使得完全匹配前 \(i\) 位变成完全匹配前 \(j\) 位的填数方案。
那么转移也就是:

\[dp[i][j] = \sum_{k=0}^{m-1} dp[i-1][k] \times g[k][j]
\]

这样就可以很完美的凑成矩阵的形式,对于 \(g\) 使用 \(KMP\) 枚举一遍就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 22;
const int M = 22;
struct matrix{
	int a[M][M];
	int n,m;
	matrix(){   //千万不要忘记初始化为 0 
		memset(a,0,sizeof(a));
		n = m = 0;
	}
}g,f;
char s[N];
int nxt[N],n,m,MOD;
matrix operator * (matrix a,matrix b){
	matrix c;
	c.n = a.n;c.m = b.m;
	memset(c.a,0,sizeof(c.a));
	for(int i=0; i<=a.n; i++)
		for(int j=0; j<=a.n; j++)
			for(int k=0; k<=a.n; k++)
				c.a[i][j] = (c.a[i][j] + a.a[i][k] * b.a[k][j])%MOD;
	return c;
}
matrix power(matrix a,int b){
	matrix res;
	res.n = res.m = a.n;
	memset(res.a,0,sizeof(res.a));
	for(int i=0; i<=res.n; i++)	res.a[i][i] = 1;
	while(b > 0){
		if(b & 1)	res = res * a;
		a = a * a;
		b >>= 1;
	}
	return res;
}
int main(){
	scanf("%d%d%d",&n,&m,&MOD);
	scanf("%s",s+1);
	//get_kmp && g
	nxt[1] = 0;
	for(int i=2,j=0; i<=m; i++){
		while(j && s[j+1] != s[i])	j = nxt[j];
		if(s[j+1] == s[i])	j++;
		nxt[i] = j;
	}
	for(int i=0; i<m; i++){   //这里保证了任意时刻都不会匹配完,因为我们只会匹配到 m-1 
		for(int j=0; j<=9; j++){
			int now = i;
			while(s[now + 1] != j + '0' && now)	now = nxt[now];
			if(s[now + 1] == j + '0')	now++;
			if(now < m)	g.a[i][now]++;
		}
	}
	g.n = g.m = m-1;
	//---------------------------------------------------
	g = power(g,n);     //因为 f 就是一个 1,0,0,0, 所以答案显然是第 0 行的和 
	int ans = 0;
	for(int i=0; i<m; i++)	ans = (ans + g.a[0][i])%MOD;
	printf("%d\n",ans);
	return 0;
}

[CTSC2014] 企鹅 QQ

题目分析:

我们可以发现一个性质:对于任意两个字符串不可能存在去掉两个及以上的位置他们都可以相等。
所以就直接枚举哪一个位置,然后每一个字符串扫一遍,做一下哈希就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e4+5;
const int L = 350;
char s[N][L];
int n,len,ALP,h[N],hashl[N][L],hashr[N][L];
void init(int x){
	for(int i=1; i<=len; i++)	hashl[x][i] = hashl[x][i-1] * 139 + s[x][i];
	for(int i=len; i>=1; i--)	hashr[x][i] = hashr[x][i+1] * 137 + s[x][i];
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld%lld",&n,&len,&ALP);
	for(int i=1; i<=n; i++){
		scanf("%s",s[i]+1);
		init(i);
	}
	int ans = 0;
	for(int i=1; i<=len; i++){
		for(int j=1; j<=n; j++)	h[j] = hashl[j][i-1] * 709 + hashr[j][i+1] * 853;  //只要变化的规则保持不变,hash 判断相同就不会出现问题 
		sort(h+1,h+n+1);
		int tmp = 1;
		for(int j=1; j<n; j++){
			if(h[j] == h[j + 1])	ans += tmp,tmp++;
			else	tmp = 1;
		}
	}
	printf("%lld\n",ans);
	return 0;
}

[POI2012]OKR-A Horrible Poem

题目分析:

我们考虑枚举循环节长度,显然长度只可能是 \(r - l + 1\) 的约数,可以考虑每次除以最小质因数就可以得到所有的约数了,对于最小质因数可以线性筛 \(O(n)\) 预处理。
我们可以类似 \(KMP\) 的思路,如果循环节长度为 \(len\),那么区间 \([l,r]\) 是符合条件的当且仅当 \([l+len,r]\)\([l,r-len]\) 相同,判断相同显然可以哈希。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int h[N],pw[N];
int mn[N],b[N],tot,prime[N];
bool flag[N];
char s[N];
int get_hash(int l,int r){
	return h[r] - h[l - 1] * pw[r - l + 1];
}
void pre_work(int n){
	h[0] = pw[0] = 1;
	for(int i=1; i<=n; i++)	h[i] = h[i-1] * 137 + s[i];
	for(int i=1; i<=n; i++)	pw[i] = pw[i-1] * 137;
	for(int i=2; i<=n; i++){
		if(!flag[i]){
			prime[++tot] = i;
			mn[i] = i;
		}
		for(int j=1; j <= tot && i * prime[j] <= n; j++){
			flag[i * prime[j]] = true;
			mn[i * prime[j]] = prime[j];
			if(i % prime[j] == 0)	break;
		}
	}
}
signed main(){
	int n;
	scanf("%d%s",&n,s+1);
	pre_work(n);
	int q;
	scanf("%d",&q);
	while(q--){
		int l,r;
		scanf("%d%d",&l,&r);
		int p = r - l + 1;
		int cnt = 0;
		while(p != 1){
			b[++cnt] = mn[p];
			p /= mn[p];
		}  
		p = r - l + 1;
		for(int i=1; i<=cnt; i++){
			int len = p / b[i];
			if(get_hash(l,r-len) == get_hash(l+len,r)){
				p /= b[i];
			}
		}
		printf("%d\n",p);
	}
	return 0;
}

[JSOI2008]火星人

题目分析:

最长公共前缀我们可以考虑二分长度然后通过哈希判断是否合法。
那么这就是要求我们支持再序列中支持插入删除一个数,并且支持查询一个区间的哈希值。
可以直接上平衡树维护,根也就是维护这一棵子树内的哈希值。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int tot,rt,ch[N][2],val[N],sz[N],key[N];
unsigned int pw[N],h[N];
int x,y,z;
char s[N];
void pushup(int now){
	h[now] = h[ch[now][0]] * pw[1 + sz[ch[now][1]]] + val[now] * pw[sz[ch[now][1]]] + h[ch[now][1]];
	sz[now] = sz[ch[now][0]] + sz[ch[now][1]] + 1;
}
int merge(int x,int y){
	if(!x || !y)	return x + y;
	if(key[x] > key[y]){
		ch[x][1] = merge(ch[x][1],y);
		pushup(x);
		return x;
	}
	else{
		ch[y][0] = merge(x,ch[y][0]);
		pushup(y);
		return y;
	}
}
void split(int now,int k,int &x,int &y){
	if(!now){
		x = y = 0;
		return;
	}
	if(sz[ch[now][0]] < k){
		x = now;
		split(ch[now][1],k - sz[ch[now][0]] - 1,ch[x][1],y);
		pushup(x);
	}
	else{
		y = now;
		split(ch[now][0],k,x,ch[y][0]);
		pushup(y);
	}
}
int newnode(int v){
	++tot;
	val[tot] = v;key[tot] = rand();sz[tot] = 1;h[tot] = v;
	return tot;
}
void insert(int k,int v){
	split(rt,k,x,y);
	rt = merge(merge(x,newnode(v)),y);
}
void delet(int k){
	split(rt,k,x,y);
	split(x,k-1,x,z);
	rt = merge(x,y);
}
unsigned int get_hash(int l,int r){
	split(rt,r,x,y);
	split(x,l-1,x,z);
	unsigned int ans = h[z];
	rt = merge(merge(x,z),y);
	return ans;
}
signed main(){
	scanf("%s",s+1);
	int n = strlen(s+1);
	pw[0] = 1;
	for(int i=1; i<=100005; i++)	pw[i] = pw[i-1] * 131;
	for(int i=1; i<=n; i++)	insert(i-1,s[i] - 'a' + 1);
	int q;
	scanf("%d",&q);
	while(q--){
		char opt[5];
		scanf("%s",opt + 1);
		if(opt[1] == 'Q'){
			int l,r;
			scanf("%d%d",&l,&r);
			int ans = 0;
			int tl = 1,tr = min(sz[rt]-r+1,sz[rt]-l+1);
			while(tl <= tr){
				int mid = (tl + tr) >> 1;
				if(get_hash(l,l + mid - 1) == get_hash(r,r + mid - 1)){
					ans = max(ans,mid);
					tl = mid + 1;
				}
				else	tr = mid - 1;
			}
			printf("%d\n",ans);
		}
		else if(opt[1] == 'R'){
			int pos;
			char d;
			cin>>pos>>d;
			delet(pos);
			insert(pos-1,d - 'a' + 1);
		}
		else if(opt[1] == 'I'){
			int pos;
			char d;
			cin>>pos>>d;
			insert(pos,d - 'a' + 1);
		}
	}
	return 0;
}

10.10

POJ3630 Phone List

题目分析:

几乎是 Trie 树的板子题。
我们直接将所有的字符串都插入到 Trie 树上,然后再每一个字符串跑一遍判断一下就好了。
注意:原题要求存在输出 NO

代码:

点击查看代码
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 2e4+5;
int rt = 1,tot = 1;
char t[N][15];
int sz[N * 15],ch[N * 15][11];
void insert(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		sz[now]++;
		if(!ch[now][s[i] - '0'])	ch[now][s[i] - '0'] = ++tot;
		now = ch[now][s[i] - '0'];
	}
	sz[now]++;
}
int check(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++)	now = ch[now][s[i] - '0'];
	return sz[now] >= 2;
}
int main(){
	int ti;
	scanf("%d",&ti);
	while(ti--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%s",t[i] + 1);
		for(int i=1; i<=n; i++)	insert(t[i]);
		bool flag = false;
		for(int i=1; i<=n; i++){
			if(check(t[i]))
				flag = true;
		}
		if(flag)	printf("NO\n");
		else	printf("YES\n");
		for(int i=1; i<=tot; i++){
			sz[i] = 0;
			for(int j=0; j<=10; j++)
				ch[i][j] = 0;
		}
		rt = 1;tot=1;
	}
	return 0;
}

UVA1401 Remember the Word

题目分析:

我们显然可以设 \(dp[i]\) 表示前缀 \(i\) 可以分成多少种形式,然后转移就是枚举最后一段在字典里的是什么然后判断一下。
但是会发现这样会 TLE,因为我们每次都需要重新匹配,但是实际上没有必要。
我们可以设 \(dp[i]\) 表示后缀 \(i\) 可以被分成多少种形式,然后枚举最开始的一段是什么,这样就可以在枚举的过程中在 Trie 树上跳,复杂度就降下去了。
虽然看上去像是 \(O(|S|^2)\),但是其实因为每一个字典的长度不会超过 \(100\),所以复杂度大约是 \(O(100 \times |S|)\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MOD = 20071027;
const int N = 5e6+5;
const int ALP = 26;
int rt=1,tot=1;
char s[N],t[N];
bool flag[N];
int f[N],ch[N][ALP];
void insert(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a']	= ++tot;
		now = ch[now][s[i] - 'a'];
	}
	flag[now] = true;
}
bool check(string s){
	int n = s.size();
	int now = rt;
	for(int i=0; i<n; i++){
		now = ch[now][s[i] - 'a'];
		if(!now)	return;
	}
	return flag[now];
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int res = 0;
	while(~scanf("%s",s+1)){
		++res;
		printf("Case %d: ",res);
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++){
			scanf("%s",t + 1);
			insert(t);
		}
		int m = strlen(s+1);
		f[m+1] = 1;
		for(int i=m; i>=1; i--){
			int now = rt;
			for(int j=i; j<=m; j++){
				if(!ch[now][s[j] - 'a'])	break;
				now = ch[now][s[j] - 'a'];
				if(flag[now])	f[i] = (f[i] + f[j+1]) % MOD;
			}
		}
		printf("%d\n",f[1]);
		for(int i=1; i<=m+1; i++) f[i] = 0;
		for(int i=1; i<=tot; i++){
			flag[i] = false;
			for(int j=0; j<ALP; j++){
				ch[i][j] = 0;
			}
		}
		rt = 1,tot = 1;
	}
	return 0;
}

UVA11732 "strcmp()" Anyone?

题目分析:

我们发现其实就是前缀与前缀去一点点匹配,所以就考虑建 Trie 树。
因为每一个字符串只与其后面的字符串可以匹配进答案,所以考虑从后到前加入。
统计答案的话就是注意:循环内部的判断等于也是一次比较。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6+5;
const int ALP = 64;
int sz[N],ch[N][ALP],flag[N],rt = 1,tot=1;
char t[4005][1005];
int get(char c){
	if(c >= 'a' && c <= 'z')	return c - 'a';
	if(c >= 'A' && c <= 'Z')	return c - 'A' + 26;
	return c - '0' + 52;
}
void insert(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		sz[now]++;
		if(!ch[now][get(s[i])])	ch[now][get(s[i])] = ++tot;
		now = ch[now][get(s[i])];
	}
	sz[now]++;flag[now]++;
}
long long calc(char *s){
	long long ans = 0;
	int n = strlen(s+1);
	int now = rt;
	for(int i = 1; i<=n; i++){
		ans += sz[now];
		if(!now)	break;
		now = ch[now][get(s[i])];
		ans += sz[now];
	}
	ans += flag[now] + sz[now];
	return ans;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	int res = 1;
	while(~scanf("%d",&n)){
		if(n == 0)	break;
		printf("Case %d: ",res);
		res++;
		for(int i=1; i<=n; i++)	scanf("%s",t[i] + 1);
		long long ans = 0;
		for(int i=n; i>=1; i--){
			ans += calc(t[i]);
			insert(t[i]);
		}
		printf("%lld\n",ans);
		for(int i=1; i<=tot; i++){
			sz[i] = flag[i] = 0;
			for(int j=0; j<ALP; j++){
				ch[i][j] = 0;
			}
		}
		rt = 1,tot = 1;
	}
	return 0;
}

[NOI Online 2021 提高组] 积木小赛

题目分析:

这怕是当年唯一一道可做的题了。
显然我们最后剩余的在 \(t\) 中一定是一段区间,而且看到 \(t\) 的长度不大,就可以考虑直接枚举哪个区间,然后贪心去和 \(s\) 匹配。
发现这样是 \(O(n^3)\) 的,但是其实可以优化。
我们考虑固定左端点,使得右端点在递增的过程中匹配的 \(s\) 的长度也一定是递增的,所以用一个指针维护一下就好了,这样就顺利优化到了 \(O(n^2)\)
对于判断某些合法区间相同的情况用一个哈希就好了。
因为最后需要去重,所以其实总复杂度是 \(O(n^2 \log n^2)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e6+5;
const int ALP = 51971;
const int MOD = 2005091020050911;
int b[N];
char s[N],t[N];
signed main(){
	int n;
	scanf("%lld",&n);
	scanf("%s%s",s+1,t+1);
	int tot = 0;
	for(int i=1; i<=n; i++){
		int now = 1;int H = 0;
		for(int j=i; j<=n; j++){
			while(now <= n && s[now] != t[j])	now++;
			if(now > n)	break;
			H = (H * ALP + t[j] - 'a' + 1)%MOD;
			b[++tot] = H;
			now++;
		}
	}
	sort(b+1,b+tot+1);
	tot = unique(b+1,b+tot+1) - b - 1;
	printf("%lld\n",tot);
	return 0;
}

10.11

[TJOI2013]单词

题目分析:

我们对所有的单词建一个 AC 自动机然后拿论文去上面跑。
如果我们现在匹配到了 \(i\) 节点,也就是说根到 \(i\) 路径上的所有点都会出现一次,那么就打一个标记,最后子树求和就是每个点最终的答案。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6+5;
const int ALP = 26;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int rt = 1,tot = 1,cnt,head[N],ch[N][ALP],fail[N],val[N],pos[N];
char s[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void insert(char *s,int p){
	int n = strlen(s + 1);
	int now = rt;
	for(int i=1; i<=n; i++){
		val[now]++;
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
		now = ch[now][s[i] - 'a'];
	}
	val[now]++;
	pos[p] = now;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			q.push(ch[rt][i]);
			fail[ch[rt][i]] = rt;
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void dfs(int now){
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		dfs(to);
		val[now] += val[to];
	}
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		scanf("%s",s+1);
		insert(s,i);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs(1);
	for(int i=1; i<=n; i++){
		printf("%d\n",val[pos[i]]);
	}
	return 0;
}

[USACO15FEB]Censoring G

题目分析:

显然我们可以维护一个栈,记录较长串里匹配到了哪一个字符对应的 AC 自动机上的哪个节点。
然后跑就好了,完全找到了一个那么就弹栈。

代码:

点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 2e6+5;
const int ALP = 26;
PII st[N];
int rt=1,tot=1,top,sz[N],ch[N][ALP],fail[N];
bool flag[N];
char s[N],t[N];
void insert(char *s){
	int now = rt;
	int n = strlen(s+1);
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
		now = ch[now][s[i] - 'a'];
	}
	flag[now] = true;sz[now] = n;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			q.push(ch[rt][i]);
			fail[ch[rt][i]] = rt;
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%s",s+1);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		scanf("%s",t+1);
		insert(t);
	}
	build();
	int now = rt;
	n = strlen(s+1);
	//记录第几个位置,匹配到了哪一个节点 
	for(int i=1; i<=n; i++){
		while(now && !ch[now][s[i] - 'a'])	now = fail[now];
		if(ch[now][s[i] - 'a'])		now = ch[now][s[i] - 'a'];
		st[++top] = {i,now};
		if(flag[now]){
			top -= sz[now];
			now = st[top].second;
			if(!top)	now = rt;
		}
	}
	for(int i=1; i<=top; i++){
		printf("%c",s[st[i].first]);
	}
	return 0;
}

[USACO12JAN]Video Game G

题目分析:

显然我们可以设 \(dp[i][j]\) 表示填好了前 \(i\) 个字符,当前在 AC 自动机的 \(j\) 节点上的最大得分。
转移的话就是枚举下一个位置填什么然后转移过去就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e3+5;
const int ALP = 3;
const int INF = 1e9+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
int cnt,tot=1,rt=1,dp[N][N],ch[N][ALP],val[N],head[N],fail[N];
char s[N];
void insert(char *s){
	int n = strlen(s + 1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'A'])	ch[now][s[i] - 'A'] = ++tot;
		now = ch[now][s[i] - 'A'];
	}
	val[now] ++;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else 	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void dfs(int now){
	for(int i=head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		val[to] += val[now];
		dfs(to);
	}
}
int main(){
	memset(dp,-0x3f,sizeof(dp));
	int n,k;
	scanf("%d%d",&n,&k);
	for(int i=1; i<=n; i++){
		scanf("%s",s+1);
		insert(s);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs(1);
	//设 dp[i][j] 表示填好了前 i 个字符,当前在状态 j 的最大得分数 
	dp[0][1] = 0;
	for(int i=0; i<=k; i++){
		for(int j=1; j<=tot; j++){
			//直接枚举当前填什么
			for(int k='A'; k<='C'; k++){
				int now = j;
				while(now && !ch[now][k - 'A'])	now = fail[now];
				if(ch[now][k - 'A'])	now = ch[now][k - 'A'];
				if(!now)	now = rt;
				dp[i+1][now] = max(dp[i+1][now],dp[i][j] + val[now]);
			} 
		}
	}
	int ans = -INF;
	for(int i=2; i<=tot; i++)	ans = max(ans,dp[k][i]);
	printf("%d\n",ans);
	return 0;
}

10.12

[JSOI2007]文本生成器

题目分析:

我们考虑如果正着 \(dp\),复杂度有点高的样子,所以就考虑补集转化一下,考虑 \(dp\) 出所有非可读文本的方案数。
其实这也就是我们不包含所有给定的单词的方案数,其实发现这个东西在 AC 自动机的 \(fail\) 树上是很好表示的:不经过所有可读文本的结束位置所在的子树的节点。
也就是设 \(dp[i][j]\) 表示确定了前 \(i\) 个字符,当前在 AC 自动机的状态 \(j\) 上的非可读文本的方案数。
总的方案数显然就是 \(26^m\),减一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
const int ALP = 26;
const int MOD = 1e4+7;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
bool flag[N];
int cnt,tot=1,rt=1,ch[N][ALP],fail[N],head[N],dp[105][10005];
char s[N];
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = (res * a)%MOD;
		a = (a * a)%MOD;
		b >>= 1;
	}
	return res;
}
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void insert(char *s){
	int n = strlen(s + 1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'A'])	ch[now][s[i] - 'A'] = ++tot;
		now = ch[now][s[i] - 'A'];
	}
	flag[now] = true;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void dfs(int now){
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		flag[to] |= flag[now];
		dfs(to);
	}
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m;
	scanf("%lld%lld",&n,&m);
	for(int i=1; i<=n; i++){
		scanf("%s",s + 1);
		insert(s);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs(1);
	dp[0][1] = 1;
	for(int i=0; i<=m; i++){  //前 i 个字符确定当前匹配到了 j 且不存在可读文本的方案数 
		for(int j=1; j<=tot; j++){
			if(flag[j])	continue;
			for(int k=0; k<ALP; k++){
				int now = j;
				while(now && !ch[now][k])	now = fail[now];
				if(ch[now][k])	now = ch[now][k];
				if(!flag[now]){
					dp[i+1][now] = (dp[i+1][now] + dp[i][j])%MOD;
				}
			}
		}
	}
	int ans = power(26,m);
	for(int i=1; i<=tot; i++)	ans = ((ans - dp[m][i])%MOD+MOD)%MOD;
	printf("%lld\n",ans);
	return 0;
}

[SDOI2014] 数数

题目分析:

发现这个和上一道题是很像的,只不过就是我们能选择的数有了限制,所以就根据上文类似的形式就可以考虑数位 \(dp\)
其余的与上文一样,就是将我们填数使用数位 \(dp\) 去弄,然后依旧是在 \(AC\) 自动机上去跑。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
const int N = 2e6+5;
const int ALP = 10;
const int MOD = 1e9+7;
using namespace std;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int rt=1,tot=1,cnt,n,f[1500][2][2][2000],head[N],ch[N][ALP],fail[N];
char s[N],num[N],flag[N],vis[1500][2][2][2000];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void insert(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - '0'])	ch[now][s[i] - '0'] = ++tot;
		now = ch[now][s[i] - '0'];
	}
	flag[now] = true;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = 0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void dfs(int now){
	for(int i=head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		flag[to] |= flag[now];
		dfs(to);
	}
}
int dfs(int dig,bool limit,bool pre_0,int now){
	//当前要分配第 dig 位,之前有没有抵到上届,是否有前导零,在AC自动机上跑到了第now个节点
	if(flag[now])	return 0;
	if(vis[dig][limit][pre_0][now])	return f[dig][limit][pre_0][now];
	if(dig == n + 1){
		if(pre_0){
			return 0;
		}
		return !flag[now];
	}
	int p = num[dig] - '0';
	int lmt = limit ? p : 9;
	int ans = 0;
	for(int i=0; i<=lmt; i++){
		if(i == 0 && pre_0){
			ans = (ans + dfs(dig+1,i == p && limit,pre_0,now))%MOD;
		}
		else{
			int g = now;
			while(now && !ch[now][i])	now = fail[now];
			if(ch[now][i])	now = ch[now][i];
			if(!now)	now = rt;
			ans = (ans + dfs(dig+1,i == p && limit,false,now))%MOD;
			now = g;
		}
	}
	vis[dig][limit][pre_0][now] = true;
	return f[dig][limit][pre_0][now] = ans;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%s",num + 1);
	n = strlen(num + 1);
	int m;
	scanf("%lld",&m);
	for(int i=1; i<=m; i++){
		scanf("%s",s+1);
		insert(s);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs(1);
	printf("%lld",dfs(1,1,1,1));
	return 0;
}

10.13

[NOI2011] 阿狸的打字机

题目分析:

多个字符串之间的匹配关系考虑 AC 自动机。
\(s\)\(t\) 中出现其实就是相当于让 \(t\) 在 Trie 树上走,在 fail 树上其经过的所有节点到根的路径上经过 \(s\) 对应点的次数就是出现次数。
但是我们发现这样做需要维护链加,复杂度就是 \(O(log^2n)\) 而且很难写,就考虑转化一下:
我们对 \(t\) 在 Trie 树上经过的节点进行单点加,而对于 \(s\) 我们做一次子树求和,这样就只需要用一个树状数组维护 \(dfs\) 序就很好实现了。
但是我们显然不能真的每一次询问都在 Trie 树上走一遍,所以我们考虑对于 Trie 树上的每个节点 \(s\) 记录询问 \(t\)\(s\) 中出现的 \(t\),然后就只需要在 Trie 树上走一遍,过程中就可以得到所有询问的答案。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int ALP = 26;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int cnt,tot = 1,tmp,rt = 1,pos[N],sum[N],ans[N],dfn[N],sz[N],back[N],head[N],ch[N][ALP],fail[N];
char s[N],t[N];
vector<pair<int,int> > g[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = 0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void dfs(int now,int fath){
	dfn[now] = ++tmp;
	sz[now] = 1;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs(to,now);
		sz[now] += sz[to];
	}
}
void modify(int x,int val){
	if(!x)	return;
	for(;x<=tmp;x+=x&(-x))	sum[x] += val;
}
int query(int x){
	if(x <= 0)	return 0;
	int ans = 0;
	for(;x;x-=x&(-x))	ans += sum[x];
	return ans;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%s",t+1);
	int h = strlen(t+1);
	int len = 0;
	int res = 0;
	int now = rt;
	for(int i=1; i<=h; i++){
		if(t[i] == 'P')	pos[++res] = now;
		else if(t[i] == 'B')	now = back[now];
		else{
			if(!ch[now][t[i] - 'a'])	ch[now][t[i] - 'a'] = ++tot;
			back[ch[now][t[i] - 'a']] = now;
			now = ch[now][t[i] - 'a'];
		}
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs(1,0);
	int q;
	scanf("%d",&q);
	for(int i=1; i<=q; i++){
		int x,y;
		scanf("%d%d",&x,&y);
		g[y].push_back({x,i});
	}
	now = rt;
	res = 0;
	for(int i=1; i<=h; i++){
		if(t[i] == 'P'){
			res++;
			for(auto p : g[res]){
				int x = pos[p.first];
				ans[p.second] = query(dfn[x] + sz[x] - 1) - query(dfn[x] - 1);
			}
		}
		else if(t[i] == 'B'){
			modify(dfn[now],-1);
			now = back[now];
		}
		else{
			now = ch[now][t[i] - 'a'];
			modify(dfn[now],1);
		}
	}
	for(int i=1; i<=q; i++){
		printf("%d\n",ans[i]);
	}
	return 0;
}

CF163E e-Government

题目分析:

看到多个字符串匹配,显然想到 AC 自动机。
我们明显需要对 \(S\) 集合建 AC 自动机,插入删除也就是对应着来就好了。
最关键的就是查询操作,考虑这个操作其实可以转化为对于给定的 \(t\) 求它有多少个前缀的后缀在 AC 自动机上。
其实就很简单了,直接拿着 \(t\) 在 Trie 树上走一遍,期间询问它到根的权值和就好了。
但是这样依旧会涉及链加,所以就考虑对于 \(S\) 集合里面的每一个字符串,其对应的节点做一个子树加,这样就可以将询问转化为单点查询,复杂度就降下去了,也更加好写了

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
const int ALP = 26;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
char s[N],t[N];
int cnt,tot = 1,rt=1,res,pos[N],head[N],ch[N][ALP],fail[N],sum[N],dep[N],fa[N],sz[N],dfn[N];
bool flag[N];
void insert(char *s,int p){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
		now = ch[now][s[i] - 'a'];
	}
	pos[p] = now;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = 0;i < ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void dfs1(int now,int fath){
	dep[now] = dep[fath] + 1;
	fa[now] = fath;
	sz[now] = 1;dfn[now] = ++res;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs1(to,now);
		sz[now] += sz[to];
	}
}
void modify(int x,int val){
	for(;x<=res;x+=x&(-x))	
		sum[x] += val;
}
int query(int x){
	if(!x)	return 0;
	int ans = 0;
	for(;x;x-=x&(-x))	ans += sum[x];
	return ans;
}
void change(int l,int r,int val){
	modify(l,val);
	modify(r+1,-val);
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,k;
	scanf("%d%d",&n,&k);
	for(int i=1; i<=k; i++){
		scanf("%s",s + 1);
		insert(s,i);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs1(1,0);
	for(int i=1; i<=k; i++)	
		change(dfn[pos[i]],dfn[pos[i]] + sz[pos[i]] - 1,1),flag[i] = true;
	for(int i=1; i<=n; i++){
		char opt;
		cin>>opt;
		if(opt == '+'){
			int h;scanf("%d",&h);
			if(!flag[h])
				change(dfn[pos[h]],dfn[pos[h]] + sz[pos[h]] - 1,1),flag[h] = true;
		}
		else if(opt == '-'){
			int h;scanf("%d",&h);
			if(flag[h])
				change(dfn[pos[h]],dfn[pos[h]] + sz[pos[h]] - 1,-1),flag[h] = false;
		}
		else if(opt == '?'){
			scanf("%s",t+1);
			int now = rt;
			int len = strlen(t+1);
			int ans = 0;
			for(int j=1; j<=len; j++){
				while(now && !ch[now][t[j] - 'a'])	now = fail[now];
				if(!now)	now = rt;
				if(ch[now][t[j] - 'a'])	now = ch[now][t[j] - 'a'];
				ans = ans + query(dfn[now]);
			}
			printf("%d\n",ans);
		}
	}
	return 0;
}

10.14

CF1207G Indie Album

题目分析:

显然我们对所有版本的串建 AC 自动机是不明智,因为复杂度也很高,也很难实现动态维护 fail 树的结构等等。
所以就考虑对于所有的询问串也就是 \(t\) 建边。
那这样其实就很好办了,就对于各个版本的串我们挨个走,走到某一个版本就来解决掉这个版本的询问。
显然若 \(t\) 在节点 \(i\) 出现,那么对应的在 \(fail\) 树中 \(i\) 的子孙也会出现,所以对于各个版本的串实现一个单点加,对于询问做一个子树求和就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
const int ALP = 26;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int tot=1,rt=1,res,cnt,head[N],ch[N][ALP],sz[N],fail[N],sum[N],dfn[N],pos[N],ans[N];
char s[N],add[N];
vector<int> Q[N],to[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void insert(char *s,int id){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
		now = ch[now][s[i] - 'a'];
	}
	pos[id] = now;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = 0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void dfs(int now,int fath){
	dfn[now] = ++res;sz[now] = 1;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs(to,now);
		sz[now] += sz[to];
	}
}
void modify(int x,int val){
	for(;x<=res;x+=x&(-x))	sum[x] += val;
}
int query(int x){
	int ans = 0;
	for(;x;x-=x&(-x))	ans += sum[x];
	return ans;	
}
int query(int l,int r){
	return query(r) - query(l-1);
}
void calc(int p,int now){
	if(p){
		while(now && !ch[now][add[p] - 'a'])	now = fail[now];
		if(ch[now][add[p] - 'a'])	now =  ch[now][add[p] - 'a'];
		if(!now) now = rt;
		modify(dfn[now],1);
	}
	for(auto i : Q[p])	
		ans[i] = query(dfn[pos[i]],dfn[pos[i]] + sz[pos[i]] - 1);
	for(auto i : to[p])	
		calc(i,now);
	modify(dfn[now],-1);
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		int opt;
		scanf("%d",&opt);
		if(opt == 1){
			char c;cin>>c;
			to[0].push_back(i);
			add[i] = c;
		}
		else{
			int p;char c;
			scanf("%d",&p);cin>>c;
			to[p].push_back(i);
			add[i] = c;
		}
	}
	int m;
	scanf("%d",&m);
	for(int i=1; i<=m; i++){
		int h;
		scanf("%d%s",&h,s+1);
		insert(s,i);
		Q[h].push_back(i);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	dfs(1,0);
	calc(0,1);  //当前是在哪个版本的串,当前在哪个节点 
	for(int i=1; i<=m; i++)	printf("%d\n",ans[i]);
	return 0;
}

[POI2000]病毒

题目分析:

显然可以考虑对于所有的病毒段建 AC 自动机。
我们考虑一点:AC 自动机的 Trie 树不仅仅是 Trie 树的作用,更可以做到失配后的跳跃。
所以其实就很显然了:我们存在一种构造方式使得在 Trie 树上跳永远跳不完,也永远不会跳到有危险的点。
其实也就是存在一个环嘛,所以直接 \(dfs\) 找一下就好了。
我们需要注意一点:如果节点 \(i\) 不可行,那么对于 \(fail\) 树上 \(i\) 的所有子树也不可行。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int ALP = 2;
const int N = 5e4+5;
int ans,tot=1,rt=1,ch[N][ALP],fail[N];
bool flag[N],use[N],vis[N];
char s[N];
void insert(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i = 1; i<=n; i++){
		if(!ch[now][s[i] - '0'])	ch[now][s[i] - '0'] = ++tot;
		flag[ch[now][s[i] - '0']] |= flag[now];
		now = ch[now][s[i] - '0'];
	}
	flag[now] = true;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i])	fail[ch[rt][i]] = rt,q.push(ch[rt][i]);
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = 0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				if(flag[fail[ch[now][i]]])	flag[ch[now][i]] = true;
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
bool calc(int now){
	vis[now] = true;
	for(int i = 0; i < ALP; i++){
		int to = ch[now][i];
		if(vis[to])	return true;
		if(flag[to] || use[to])	continue;
		use[to] = true;
		if(calc(to))	return true;
	}
	vis[now] = false;
	return false;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		scanf("%s",s+1);
		insert(s);
	}
	build();
//	for(int i=1; i<=tot; i++){
//		for(int j=0; j<ALP; j++){
//			if(ch[i][j])	printf("%d %d\n",i,ch[i][j]);
//		}
//	}
	if(calc(1))	printf("TAK\n");
	else	printf("NIE\n");
	return 0;
}

CF1202E You Are Given Some Strings...

题目分析:

看到那么美妙的形式,显然可以想到枚举分界点。
这样问题就转化为了求 \(f[i]\) 表示以 \(i\) 点结束的字符串个数,以及以 \(i\) 点开始的字符串个数,但是显然字符串翻转之后两个问题就等价了。
显然我们需要对 \(s\) 建 AC 自动机,我们考虑若 \(s\)\(i\) 状态出现那么显然在 \(fail\) 树中 \(i\) 的所有子树中也会出现,所以就做一个前缀和就好了。
然后对于 \(t\) 就放到 AC 自动机上跑,对于跑到的位置的权值就是我们对应的位置的答案。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
const int ALP = 26;
struct ACAM{
	int rt=1,tot=1,f[N],ch[N][ALP],fail[N],val[N];
	void insert(char *s){
		int n = strlen(s+1);
		int now = rt;
		for(int i=1; i<=n; i++){
			if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
			now = ch[now][s[i] - 'a'];
		}
		val[now]++;
	}
	void build(){
		queue<int> q;
		for(int i=0; i<ALP; i++){
			if(ch[rt][i]){
				fail[ch[rt][i]] = rt;
				val[ch[rt][i]] += val[rt];
				q.push(ch[rt][i]);
			}
			else	ch[rt][i] = rt;
		}
		while(!q.empty()){
			int now = q.front();q.pop();
			for(int i=0; i<ALP; i++){
				if(ch[now][i]){
					fail[ch[now][i]] = ch[fail[now]][i];
					val[ch[now][i]] += val[fail[ch[now][i]]];
					q.push(ch[now][i]);
				}
				else	ch[now][i] = ch[fail[now]][i];
			}
		}
	}
	void query(char *s){
		int n = strlen(s+1);
		int now = rt;
		for(int i=1; i<=n; i++){
			while(now && !ch[now][s[i] - 'a'])	now = fail[now];
			if(ch[now][s[i] - 'a'])	now = ch[now][s[i] - 'a'];
			if(!now)	now = rt;
			f[i] = val[now];	
		}
	}
}ac,ac2;
char s[N],t[N];
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%s",s+1);
	int n;
	scanf("%lld",&n);
	for(int i=1; i<=n; i++){
		scanf("%s",t+1);
		int m = strlen(t+1);
		ac.insert(t);
		reverse(t+1,t+m+1);
		ac2.insert(t);
	}
	ac.build();ac2.build();
	ac.query(s);
	n = strlen(s+1);reverse(s+1,s+n+1);
	ac2.query(s);
	int ans = 0;
	for(int i=1; i<n; i++)	
		ans = ans + ac.f[i] * ac2.f[n - i];
	printf("%lld\n",ans);
	return 0;
}

[HNOI2004] L 语言

题目分析:

首先这个题 \(O(m|t||s|)\) 的做法是很好想的,这里就是提供一种更强的做法;
我们的状态显然是 \(f[i]\) 表示 \(i\) 这个前缀能否被表示。
转移就是枚举前面多少个,然后判断中间的一段是否出现,但是其实我们发现 \(|s|\) 很小,所以其实只需要枚举后 \(|s|\) 个就可以。
这其实也其实我们,我们是否可以通过压位快速知道后 \(|s|\) 个有没有合法的那?当然是可以的。

我们考虑对于所有的单词建 AC 自动机,对于每一个位置记 \(g[i]\) 表示 \(i\) 这个位置出现了长度为多少的单词,其中如果二进制下 \(g[i]\) 的第 \(i\)\(1\) 代表有长度为 \(i\) 的单词。
显然可以发现若节点 \(i\) 可以出现,那么对于 \(fail\) 树上 \(i\) 的所有父亲也一定可以出现,所以求一个树上的前缀或就好了。
我们再动态维护 \(x\) 表示当前节点前 \(|s|\) 个位置的 \(f\) 值,每次将 \(t\) 放在 AC 自动机上跑,如果发现 \(x & g[i]\) 也就是意味着存在一个位置是合法的,那么就直接转移了。
所以转移就可以做到 \(O(1)\),所以总复杂度就做到了 \(O(m|t|)\)
当然可以考虑使用 \(bitset\),但是实测不会变快。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e6+5;
const int ALP = 26;
int tot=1,rt=1,g[N],ch[N][ALP],fail[N],f[N];
char s[N];
void insert(char *s){
	int n = strlen(s+1);
	int now = rt;
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
		now = ch[now][s[i] - 'a'];
	}
	g[now] |= 1<<(n-1);
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			g[ch[rt][i]] |= g[rt];
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				g[ch[now][i]] |= g[ch[fail[now]][i]];
				q.push(ch[now][i]); 
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++){
		scanf("%s",s+1);
		insert(s);
	}
	build();
	while(m--){
		int tmp = (1<<22)-1;
		scanf("%s",s+1);
		n = strlen(s+1);
		int now = rt;
		int x = 1;  //认为 f[0] = 1 
		for(int i=1; i<=n; i++){
			while(now && !ch[now][s[i] - 'a'])	now = fail[now];
			if(ch[now][s[i] - 'a'])	now = ch[now][s[i] - 'a'];
			if(!now)	now = rt;
			if(x & g[now])	f[i] = 1;
			else	f[i] = 0;
			x = (x<<1) | f[i];
			x = x & tmp;  //防止超了直接爆掉 
		}
		int ans = 0;
		for(int i=n; i>=1; i--){
			if(f[i]){
				ans = i;
				break;
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

[COCI2015]Divljak

题目分析:

显然我们需要建 AC 自动机。
我们不可能对 \(Bob\) 建 AC 自动机,因为这需要支持动态维护 \(fail\) 树,所以就对 \(Alice\) 建 AC 自动机。
考虑对于 \(Bob\) 每次新加入一个字符串,就可以理解为将这个字符串放在 AC 自动机上跑,因为我们求的是多少个字符串包含 \(S_x\),而不是出现次数所以就不能直接链加,而是要考虑转化为树链的并,但是我们发现其实这样复杂度也很高,所以就考虑继续转化,我们不进行链加,而是差分后进行单点加,而对于询问进行子树求和就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int ALP = 26;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int tot=1,rt=1,res,cnt,a[N],pos[N],head[N],dfn[N],ch[N][ALP],fail[N],son[N],fa[N],top[N],sz[N],dep[N],sum[N];
char s[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void insert(char *s,int id){
	int now = rt;
	int n = strlen(s+1);
	for(int i=1; i<=n; i++){
		if(!ch[now][s[i] - 'a'])	ch[now][s[i] - 'a'] = ++tot;
		now = ch[now][s[i] - 'a'];
	}
	pos[id] = now;
}
void build(){
	queue<int> q;
	for(int i=0; i<ALP; i++){
		if(ch[rt][i]){
			fail[ch[rt][i]] = rt;
			q.push(ch[rt][i]);
		}
		else	ch[rt][i] = rt;
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=0; i<ALP; i++){
			if(ch[now][i]){
				fail[ch[now][i]] = ch[fail[now]][i];
				q.push(ch[now][i]);
			}
			else	ch[now][i] = ch[fail[now]][i];
		}
	}
}
void dfs1(int now,int fath){
	fa[now] = fath;dep[now] = dep[fath] + 1;
	sz[now] = 1;son[now] = 0;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs1(to,now);
		sz[now] += sz[to];
		if(sz[to] > sz[son[now]])	son[now] = to;
	}
}
void dfs2(int now,int topf){
	top[now] = topf;
	dfn[now] = ++res;
	if(son[now])	dfs2(son[now],topf);
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fa[now] || to == son[now])	continue;
		dfs2(to,to);
	}
}
int lca(int x,int y){
	while(top[x] != top[y]){
		if(dep[top[x]] < dep[top[y]])	swap(x,y);
		x = fa[top[x]];
	}
	if(dep[x] > dep[y])	swap(x,y);
	return x;
}
bool cmp(int a,int b){
	return dfn[a] < dfn[b];
}
void modify(int x,int val){
	for(;x <= res;x += x & (-x))	sum[x] += val;
}
int query(int x){
	int ans = 0;
	for(;x;x -= x & (-x))	ans += sum[x];
	return ans;
}
int query(int l,int r){
	return query(r) - query(l-1);
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		scanf("%s",s+1);
		insert(s,i);
	}
	build();
	for(int i=2; i<=tot; i++)	add_edge(fail[i],i);
	
	dfs1(1,0);
	dfs2(1,1);
	int q;
	scanf("%d",&q);
	for(int i=1; i<=q; i++){
		int opt;
		scanf("%d",&opt);
		if(opt == 1){
			scanf("%s",s+1);
			int now = rt;n = strlen(s+1);
			int tmp = 0;
			for(int j=1; j<=n; j++){
				now = ch[now][s[j] - 'a'];
				a[++tmp] = now;
			}
			sort(a+1,a+tmp+1,cmp);
			for(int j=1; j<=tmp; j++)	modify(dfn[a[j]],1);
			for(int j=1; j<tmp; j++)	modify(dfn[lca(a[j],a[j+1])],-1);
		}
		else{
			int x;
			scanf("%d",&x);x = pos[x];
			printf("%d\n",query(dfn[x],dfn[x] + sz[x] - 1));
		}
	}
	return 0;
}

[HAOI2011]Problem b

题目分析:

我们设 \(g(n,m) = \sum_{i=1}^n \sum_{j=1}^m [gcd(i,j) = k]\),那么原问题显然可以转化为:\(g(n,m) - g(n-1,m) - g(n,m-1) + g(n-1,m-1)\)
那么对于一个求就是经典的莫反了。
以下均默认 \(n < m\)

\[\begin{aligned}
&= \sum_{i=1}^n \sum_{j=1}^m [gcd(i,j) = k] \\
&= \sum_{i=1}^{\lfloor \dfrac{n}{k} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{k} \rfloor} [gcd(i,j) = 1] \\
&= \sum_{i=1}^{\lfloor \dfrac{n}{k} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{k} \rfloor} \sum_{d | i \and d | j} \mu(d) \\
&= \sum_{d=1}^n \mu(d) \sum_{i=1}^{\lfloor \dfrac{n}{dk} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{dk} \rfloor} 1 \\
&= \sum_{d=1}^n \mu(d) \lfloor \dfrac{n}{dk} \rfloor \times \lfloor \dfrac{m}{dk} \rfloor
\end{aligned}
\]

那么预处理 \(\mu\) 的前缀和之后整除分块一下就可以做到 \(O(n)\) 预处理,\(O(\sqrt{n})\) 询问了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int k,cnt,mu[N],prime[N],pre[N];
bool flag[N];
void pre_work(int mx){
	mu[1] = 1;
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			prime[++cnt] = i;
			mu[i] = -1;
		}
		for(int j=1; j<=cnt && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0)	break;
			else	mu[i * prime[j]] = -mu[i];
		}
	}
	pre[0] = 0;
	for(int i=1; i<=mx; i++)	pre[i] = pre[i-1] + mu[i];
}
int calc(int n,int m){
	int ans = 0;
	int mx = min(n,m);
	for(int l=1,r;l<=mx;l = r + 1){
		r = min(n / (n / l),m / (m / l));
		ans = ans + (pre[r] - pre[l-1]) * (n / l) * (m / l); 
	}
	return ans;
}
int main(){
	pre_work(100000);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		int a,b,c,d;
		scanf("%d%d%d%d%d",&a,&b,&c,&d,&k);
		printf("%d\n",calc(b/k,d/k) - calc(b/k,(c-1)/k) - calc((a-1)/k,d/k) + calc((c-1)/k,(a-1)/k));
	}
	return 0;
}

[牛客挑战赛64] A.Connect Graph

题目分析:

我们只需要使用并查集维护一下连通性,当与 \(1\) 联通时更新一下答案就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+5;
int fa[N],ans[N];
vector<int> son[N];
int find(int x){
	if(fa[x] == x)	return x;
	return fa[x] = find(fa[x]);	
}
void merge(int u,int v,int val){
	if(find(u) == find(v))	return;
	u = find(u),v = find(v);
	if(son[u].size() < son[v].size())	swap(u,v);
	if(find(1) == find(u))	for(int i : son[v])	ans[i] = val;
	if(find(1) == find(v))	for(int i : son[u])	ans[i] = val;
	for(int i : son[v])	son[u].push_back(i);
	fa[v] = u;
	son[v].clear();
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++)	fa[i] = i,son[i].push_back(i);
	for(int i=1; i<=n; i++)	ans[i] = -1;
	ans[1] = 0;
	int m1,m2;
	scanf("%d%d",&m1,&m2);
	for(int i=1; i<=m1; i++){
		int u,v;
		scanf("%d%d",&u,&v);
		merge(u,v,0);
	}
	for(int i=1; i<=m2; i++){
		int u,v;
		scanf("%d%d",&u,&v);
		merge(u,v,i);
	}
	for(int i=1; i<=n; i++)
		printf("%d\n",ans[i]);
	return 0;
}

[牛客挑战赛64] B.Hanoi Tower

题目分析:

对于输出方案我们可以类似汉诺塔的思想,一个一个去移动。
而对于输出方案数我们可以打表然后观察出以下递推式:\(f[i] = 2\times f[i-1] + (i \% 3 \not= 2)\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 200;
int f[N],b[N];
void dfs(int id,int pos){
	if(b[id] == pos)	return;
	for(int i=id-1; i>=1; i--)	dfs(i,3 - b[id] - pos);   //神奇的发现这个就是可以找到 b[id] 和 pos 之外的那一个
	b[id] = pos;printf("%d %c\n",id,pos + 'A'); 
}
int main(){
	int n;
	scanf("%d",&n);
	f[1] = 1,f[2] = 2;
	for(int i=3; i<=n; i++){
		f[i] = f[i-1] * 2 + (i % 3 != 2);
	}
	printf("%d\n",f[n]);
	if(n <= 20){
		for(int i=2; i<=n; i+=2)	b[i] = 2;
		for(int i=n; i>=1; i--)	dfs(i,1);
	}
	return 0;
}

[牛客挑战赛64] C.Rolling Girl

题目分析:

我们知道有一个经典的结论:对于一个固定起点 \(s\),长度为 \(n\) 的环,每次跳 \(k\) 步,其经过的点一定是模 \(\gcd(n,k)\)\(s\) 同余的数。
所以回归到这个题,就是 \(s = 1\),然后 \(k\)\([1,n]\) 而已,我们就考虑计数。
也就是枚举 \(\gcd\) 是多少个,然后计算 \(\gcd\) 等于这个数的 \(k\) 的个数,然后暴力计算这个 \(\gcd\) 时的答案。
复杂度完全可以过。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7+5;
const int MOD = 1004535809;
int tot,a[N],phi[N],prime[N];
bool flag[N];
unsigned seed, mod; 
unsigned read() {
    seed ^= seed << 13; 
    seed ^= seed >> 5; 
    seed ^= seed << 7; 
    return seed % mod + 1; 
}
void pre_work(int mx){
	phi[1] = 1;
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			phi[i] = i-1;
			prime[++tot] = i;
		}
		for(int j=1; j<=tot && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0){
				phi[i * prime[j]] = phi[i] * prime[j];
			}
			else	phi[i * prime[j]] = phi[i] * phi[prime[j]];
		}
	}
}
int calc(int i,int n){
	int mn = a[0];
	int tot = 0;
	for(int j = (i == n ? 0 : i); j != 0 ; j = (j + i)%n){
		mn = a[j] < mn ? a[j] : mn;
		tot++;
	}
	++tot;
	return (tot * mn)%MOD;
}
signed main(){
	pre_work(10000000);
	int n;
	scanf("%lld",&n);
	int p = n;
	cin>>seed>>mod;
	for(int i=1; i<=n; i++)	a[i] = read();
	int ans = 0;
	a[0] = a[n];
	for(int i=1; i<=n; i++){  //i 其实就是枚举的 gcd 
		if(n % i == 0){
			int h = n / i;
			ans = (ans + phi[n/i] * calc(i,n))%MOD;
		}
	}
	printf("%lld\n",ans);
	return 0;
}

10.15

[国家集训队]Crash的数字表格 / JZPTAB

题目分析:

其实题目就是让我们求这个式子的值:

\[\sum_{i=1}^n \sum_{j=1}^m lcm(i,j)
\]

那么根据:\(lcm(i,j) = \dfrac{i \times j}{\gcd(i,j)}\) 我们就可以愉快地化式子了。

\[\begin{aligned}
&=\sum_{i=1}^n \sum_{j=1}^m lcm(i,j) \\
&=\sum_{i=1}^n \sum_{j=1}^m \dfrac{i \times j}{\gcd(i,j)} \\
&=\sum_{d=1}^{n} d \times \sum_{i=1}^{\lfloor \dfrac{n}{d} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{d} \rfloor} [gcd(i,j) = 1] \times i \times j \\
\end{aligned}
\]

我们其实发现后面这一坨非常规整,所以就可以考虑去解决这个子问题:
也就是设:

\[sum(n,m) = \sum_{i=1}^n \sum_{j=1}^m [gcd(i,j) = 1] \times i \times j
\]

那么也可以非常经典的转化为:

\[\begin{aligned}
&=\sum_{d'=1}^n \mu(d') \sum_{i=1}^{\lfloor \dfrac{n}{d'} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{d'} \rfloor} d'i \times d'j \\
&=\sum_{d'=1}^n \mu(d') \times d'^2 \sum_{i=1}^{\lfloor \dfrac{n}{d'} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{d'} \rfloor} i \times j
\end{aligned}
\]

发现最后面这一坨可以很轻松地算出来:

\[\begin{aligned}
g(n,m)
&= \sum_{i=1}^{n} \sum_{j=1}^{m} i \times j \\
&= (\sum_{i=1}^n i) \times (\sum_{j=1}^m j) \\
&= \dfrac{i \times (i+1)}{2} \times \dfrac{j \times (j+1)}{2}
\end{aligned}
\]

对于 \(g(n,m)\) 可以 \(O(1)\) 计算,对于 \(sum(n,m)\) 可以预处理出来 \(\mu(i) \times i^2\) 的前缀和,整除分块 \(O(\sqrt{n})\) 计算,对于原式可以 \(O(\sqrt{n})\) 整除分块。
所以总时间复杂度 \(O(n)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e7+5;
const int MOD = 20101009;
int tot,prime[N],mu[N],pre[N];
bool flag[N];
int mod(int x){
	return ((x % MOD)+MOD)%MOD;
}
void pre_work(int mx){
	mu[1] = 1;
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			prime[++tot] = i;
			mu[i] = -1;
		}
		for(int j=1; j<=tot && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0) break;
			else	mu[i * prime[j]] = -mu[i];
		}
	}
	pre[0] = 0;
	for(int i=1; i<=mx; i++)	pre[i] = mod(pre[i-1] + mod(mod(mu[i] * i) * i));
}
int g(int n,int m){
	return mod(mod(n * (n + 1) / 2) * mod(m * (m + 1) / 2));
}
int sum(int n,int m){
	int ans = 0;
	for(int l=1,r; l <= min(n,m); l = r + 1){
		r = min(n/(n/l),m/(m/l));
		ans = mod(ans + (pre[r] - pre[l-1]) * g(n/l,m/l));
	}
	return ans;
}
int calc(int n,int m){
	int ans = 0;
	for(int l=1,r; l <= min(n,m); l = r + 1){
		r = min(n/(n/l),m/(m/l));
		ans = mod(ans + mod((r - l + 1) * (l + r) / 2)* sum(n/l,m/l));  
	}
	return ans;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m;
	scanf("%lld%lld",&n,&m);
	pre_work(max(n,m) + 5);
	printf("%lld\n",calc(n,m));
	return 0;
}

[SDOI2015]约数个数和

题目分析:

我们可以发现一个很高级的性质:

\[d(n,m) = \sum_{i|n} \sum_{j|m} [\gcd(i,j) = 1]
\]

那么显然就推一推就好了。

\[\begin{aligned}
&=\sum_{i=1}^n \sum_{j=1}^m \sum_{i' | i} \sum_{j' | j} [\gcd(i',j') = 1] \\
&=\sum_{i'=1}^n \sum_{j'=1}^m [\gcd(i',j') = 1] \sum_{i=1}^{\lfloor \dfrac{n}{i'} \rfloor} \sum_{j=1}^{\lfloor \dfrac{m}{j'} \rfloor} 1 \\
&=\sum_{i'=1}^n \sum_{j'=1}^m [\gcd(i',j') = 1] \times \lfloor \dfrac{n}{i'} \rfloor \times \lfloor \dfrac{m}{j'} \rfloor \\
&=\sum_{d=1}^n \mu(d) \sum_{i'=1}^{\lfloor \dfrac{n}{d} \rfloor} \sum_{j' = 1}^{\lfloor \dfrac{m}{d} \rfloor} \lfloor \dfrac{n}{i'} \rfloor \times \lfloor \dfrac{m}{j'} \rfloor \\
&=\sum_{d=1}^n \mu(d) (\sum_{i'=1}^{\lfloor \dfrac{n}{d} \rfloor} \lfloor \dfrac{n}{i'} \rfloor) \times (\sum_{j' = 1}^{\lfloor \dfrac{m}{d} \rfloor} \lfloor \dfrac{m}{j'} \rfloor)
\end{aligned}
\]

所以考虑维护 \(\mu\) 的前缀和,以及 $\lfloor \dfrac{n}{d} \rfloor $ 的和。
这个维护可以整除分块一下,然后最后原式也可以整除分块一下,所以复杂度就是 \(O(n\sqrt{n})\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
int tot,prime[N],mu[N],pre[N],g[N];
bool flag[N];
void pre_work(int mx){
	mu[1] = 1;
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			prime[++tot] = i;
			mu[i] = -1;
		}
		for(int j=1; j<=tot && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0)	break;
			else	mu[i * prime[j]] = -mu[i];
		}
	}
	pre[0] = 0;
	for(int i=1; i<=mx; i++)	pre[i] = pre[i-1] + mu[i];
	for(int i=1; i<=mx; i++){  //预处理 T/i 的和的式子,方便后续计算 
		for(int l=1,r=0; l<=i; l = r + 1){
			r = i / (i / l);
			g[i] = g[i] + (r - l + 1) * (i / l);
		}
	}
}
int calc(int n,int m){
	int ans = 0;
	for(int l=1,r; l <= min(n,m); l = r + 1){
		r = min(n/(n/l),m/(m/l));
		ans = ans + (pre[r] - pre[l-1]) * g[n/l] * g[m/l];
	}
	return ans;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	pre_work(50000);
	int t;
	scanf("%lld",&t);
	while(t--){
		int n,m;
		scanf("%lld%lld",&n,&m);
		printf("%lld\n",calc(n,m));
	}
	return 0;
}

P6222 「P6156 简单题」加强版

题目分析:

直接开始推式子啦。

\[\begin{aligned}
&=\sum_{i=1}^n \sum_{j=1}^n (i+j)^k \gcd(i,j) \mu^2(\gcd(i,j)) \\
&=\sum_{d=1}^n \sum_{i=1}^{\big\lfloor \dfrac{n}{d} \big\rfloor} \sum_{j=1}^{\big\lfloor \dfrac{n}{d} \big\rfloor} [\gcd(i,j) = 1](di+dj)^k \times d \times \mu^2(d) \\
&=\sum_{d=1}^n d^{k+1} \mu^2(d )\sum_{i=1}^{\big\lfloor \dfrac{n}{d} \big\rfloor} \sum_{j=1}^{\big\lfloor \dfrac{n}{d} \big\rfloor} [\gcd(i,j) = 1](i+j)^k \\
&=\sum_{d=1}^n d^{k+1} \mu^2(d) \sum_{t=1}^{\big\lfloor \dfrac{n}{d} \big\rfloor} t^k \mu(t) \sum_{i=1}^{\big\lfloor \dfrac{n}{td} \big\rfloor} \sum_{j=1}^{\big\lfloor \dfrac{n}{td} \big\rfloor} (i+j)^k \\
&=\sum_{T=1}^n T^k \bigg(\sum_{d|T} d \mu^2(d) \mu(\dfrac{T}{d})\bigg)\bigg(\sum_{i=1}^{\big\lfloor \dfrac{n}{T} \big\rfloor} \sum_{j=1}^{\big\lfloor \dfrac{n}{T} \big\rfloor} (i+j)^k \bigg)
\end{aligned}
\]

对于最后这一坨也就是 \((i+j)^k\) 这些
考虑设以下几个值:

\[\begin{aligned}
F_{n} &= \sum_{i=1}^n i^k \\
G_{n} &= \sum_{i=1}^n F_i \\
S_{n}
&= \sum_{i=1}^{n} \sum_{j=1}^{n} (i+j)^k \\
&= \sum_{i=1}^{n} (F_{n+i} - F_i) \\
&= \sum_{i=n+1}^{2n} F_i - \sum_{i=1}^n F_i \\
&= G_{2n} - 2G_n
\end{aligned}
\]

对于 \(i^k\) 我们可以直接线性筛就出来了。

对于前面那些我们可以考虑:
设:

\[f_n = \sum_{d|T} d \mu^2(d) \mu(\dfrac{T}{d})
\]

显然 \(f_n\) 是好几个积性函数卷积的形式,所以 \(f_n\) 也是一个积性函数,那么讨论一下 \(f(p^k)\) 的取值就可以了。

  1. \(k = 0\)\(f(1) = 1\)
  2. \(k = 1\)\(f(p) = p-1\)
  3. \(k = 2\)\(f(p^2) = -p\)
  4. \(k \ge 3\):一定存在某一个 \(\mu = 0\) 所以值就是 \(0\)

具体推导就是直接代数进去算就好了,所以也可以使用线性筛求出 \(f\) 函数的值

所以我们预处理的复杂度大约为 \(O(n)\)
考虑我们回答一次询问其实就可以考虑直接数论分块,所以复杂度为 \(O(\sqrt{n})\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int unsigned   //这个模数相当于自然溢出 
using namespace std;
const int N = 1e7+5;
int f[N << 1],F[N << 1];
vector<int> prime;
bitset< N<<1 > flag;
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = res * a;
		a = a * a;
		b >>= 1;
	}
	return res;
}
void pre_work(int mx,int k){
	f[1] = F[1] = 1;
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			prime.push_back(i);
			f[i] = i-1;F[i] = power(i,k);
		}
		for(int j : prime){
			if(i * j > mx)	break;
			flag[i * j] = true;
			F[i * j] = F[i] * F[j];
			if(i % j == 0){
				int h = i / j;
				if(h % j)  //这样就说明只是二次方,没有更多了 
					f[i * j] = f[h] * (-j);
				break;
			}
			else	f[i * j] = f[i] * (j - 1);
		}
	}
	for(int i=1; i<=mx; i++)	f[i] = f[i-1] + f[i] * F[i];  //先乘上 T^k
	for(int i=1; i<=mx; i++)	F[i] = F[i] + F[i-1];
	for(int i=1; i<=mx; i++)	F[i] = F[i] + F[i-1];
}
int calc(int n){
	return F[2*n] - 2 * F[n];
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int T,N,K;
	cin>>T>>N>>K;
	pre_work(N<<1,K);
	while(T--){
		int n;
		cin>>n;
		int ans = 0;
		for(int l=1,r=0; l<=n; l = r + 1){
			r = n / (n / l);
			ans = ans + (f[r] - f[l-1]) * calc(n / l);
		}
		cout<<ans<<'\n';
	}
	return 0;
}

10.16

CF1746 A. Maxmina

题目分析:

显然只要有 \(1\) 就可以做到。
如果剩余的多于 \(k\) 个,就可以通过 \(2\) 操作一直删掉一个值,只要留一个 \(1\) 就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		int n,k;
		scanf("%d%d",&n,&k);
		int ans = 0;
		for(int i=1; i<=n; i++){
			int a;
			scanf("%d",&a);
			ans |= a;
		}
		if(ans)	printf("YES\n");
		else	printf("NO\n");
	}
	return 0;
}

CF1746 B. Rebellion

题目分析:

显然可以考虑枚举分界点。
左边就都变成 \(0\),右边都变成大于等于 \(1\) 的数。
那么就直接维护一个前缀 \(1\) 的个数和后缀 \(0\) 的个数就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int a[N],pre[N],suf[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++)	pre[i] = pre[i-1] + (a[i] == 1);
		for(int i=n; i>=1; i--)	suf[i] = suf[i+1] + (a[i] == 0);
		int ans = 10000000;
		for(int i=0; i<=n; i++){  //枚举分界点 
			//[1,i]   [i+1,n]
			if(pre[i] >= suf[i+1]){
				ans = min(ans,pre[i]);
			}
		}
		printf("%d\n",ans);
		for(int i=1; i<=n; i++)	pre[i] = suf[i] = 0;
	}
	return 0;
}

CF1746 C. Permutation Operations

题目分析:

我们可以发现神奇的一点:给定的是一个排列。
也就是说我们一定可以给他们全部加到 \(n+1\) 这个数。
因为我们其实可以将后缀加看作一个单点加,因为我们最后一定可以让他们相等,也就是逆序对为 \(0\),所以后缀多加一点也不会有影响。

但是如果不是排列也可以做:
可以考虑维护差分数组,一个后缀加就相当于差分数组单点修改,考虑这个点的修改用一个大的值+若干小的值肯定是最优的,所以就也可以过了。

代码:

思路一:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int a[N],pos[N];
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++)	pos[n - a[i] + 1] = i;
		for(int i=1; i<=n; i++){
			printf("%d ",pos[i]);
		}
		printf("\n");
	}
	return 0;
}

思路二:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
int a[N],pos[N];
pair<int,int> b[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	a[i] = 0,b[i] = {0,0},pos[i] = 1;
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++)	b[i] = {a[i] - a[i-1],i};
		int r = n,l = 1;
		sort(b+1,b+n+1);
		for(int i=1; i<=n; i++){
			if(b[i].first < 0){
				b[i].first += r;
				r--;
				pos[r+1] = b[i].second;
				while(b[i].first < 0){
					b[i].first += l;
					l++;
					pos[l-1] = b[i].second;
				}
			}	
		}
		for(int i=1; i<=n; i++)	printf("%d ",pos[i]);
		printf("\n");
	}
	return 0;
}

CF1746 D. Paths on the Tree

题目分析:

考虑一开始肯定是将 \(k\) 条边平均分配给几个儿子。
然后剩余的几条边考虑权值最大的几条路径去选择。
可以显然发现,按照这种策略一定可以满足题目条件,也一定是最优解。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int ans = 0,cnt,head[N],fa[N],val[N],son[N];
multiset<int> st[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void dfs(int now,int k){
	if(!son[now])	st[now].insert(0);
	ans += k * val[now];
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		dfs(to,k/son[now]);
		if(!st[to].empty()){
			multiset<int>::iterator it = st[to].end();it--;
			st[now].insert(*it + val[to]);
		}
	}
	multiset<int>::iterator it = st[now].begin();
//	printf("%lld:",now);
//	while(it != st[now].end()){
//		printf("%lld ",*it);
//		it++;
//	}
//	printf("\n");
	if(son[now]){
		int p = k % son[now];
		for(int i=1; i<=p; i++){
			multiset<int>::iterator it = st[now].end();it--;
			ans = ans + *it;
			st[now].erase(it);
		}
	}
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%lld",&t);
	while(t--){
		int n,k;
		scanf("%lld%lld",&n,&k);
		for(int i=2; i<=n; i++){
			scanf("%lld",&fa[i]);
			add_edge(fa[i],i);
			son[fa[i]]++;
		}
		for(int i=1; i<=n; i++)	scanf("%lld",&val[i]);
		dfs(1,k);
		printf("%lld\n",ans);
		for(int i=1; i<=n; i++)	st[i].clear();
		for(int i=1; i<=n; i++)	head[i] = 0,son[i] = 0;
		cnt = ans = 0;
	}
	return 0;
}

CF1746 E1. Joking (Easy Version)

题目分析:

首先考虑如何解决输出错误信息这个问题:
如果连续两次都可以说明同一个事实,那么说明这个事就是真实的。

考虑若当前候选值只剩 \(2\) 个,显然直接问就好了。

如果候选值只剩 \(3\) 个,可以按以下格式询问:

? a[0]
? a[1]
? a[1]
? a[0]

可以发现,我们其实可以根据这四次的结果判断出来哪个值不可以被选择。

那如果不止剩 \(3\) 个呢?
考虑将这些数均匀分成四个集合,记为 \(V_1,V_2,V_3,V_4\),询问:\(V_1 \cup V_2\)\(V_1 \cup V_3\)
显然可以根据询问的结果删掉一个集合。

所以最终的询问次数可以过。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
bool query(vector<int> v){
	printf("? %d ",v.size());
	for(int i : v)	printf("%d ",i);
	printf("\n");
	string s;cin>>s;
	return s == "YES";
}
vector<int> insert(const vector<int> a,vector<int> b){
	vector<int> tmp;
	for(int i : a)	tmp.push_back(i);
	for(int i : b)	tmp.push_back(i);
	return tmp;
}
void solve(vector<int> v){
	if(v.size() <= 2){
		printf("! %d\n",v[0]);
		string s;cin>>s;
		if(s == ":)")	return;
		printf("! %d\n",v[1]);
		return;
	}
	else if(v.size() == 3){
		bool is[4];
		is[0] = query({v[0]});
		is[1] = query({v[1]});
		is[2] = query({v[1]});
		is[3] = query({v[0]});
		if(is[1] && is[2])	solve({v[1]});  //这个推导过程需要在纸上画一下自己感觉一下 
		else if(!is[1] && !is[2])	solve({v[0],v[2]});
		else if((is[0] && is[1]) || (is[2] && is[3]))	solve({v[0],v[1]});
		else if((!is[0] && is[1]) || (is[2] && !is[3]))	solve({v[1],v[2]});
		else	solve({v[0],v[2]});
	}
	else{
		vector<int> v1,v2;v1.clear();v2.clear();;
		for(int i=0; i<v.size(); i++){
			if(i & 1)	v1.push_back(v[i]);
			if(i & 2)	v2.push_back(v[i]);
		}
		bool is[2];is[0] = is[1] = false;
		vector<int> tmp;
		is[0] = query(v1);
		is[1] = query(v2);
		tmp.clear();
		for(int i = 0; i<v.size(); i++){
			if((!(i & 1) ^ is[0]) || (!(i&2) ^ is[1]))
				tmp.push_back(v[i]);
			//考虑 对+错:舍弃 v3
			//考虑 对+对:舍弃 v4
			//考虑 错+错:舍弃 v1
			//考虑 错+对:舍弃 v2 
			//推荐自己推一下,确实这样是可行的 
			//v1:x&1 && x&2 
			//v2:!x&1 && x&2
			//v3:x&1 && !x&1
			//v4:!x&1 && !x&2
		}
		solve(tmp);
	}
}
int main(){
	vector<int> tmp;
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++)	tmp.push_back(i);
	solve(tmp);
}

10.17

CF645F Cowslip Collections

题目分析:

我们设 \(mx\) 为加入的数的最大值。
我们考虑设 \(f(i)\) 表示 \(\gcd\)\(i\) 的方案数,显然答案为:\(\sum_{i=1}^{mx} i \times f(i)\)
发现这个其实不好计算,但是有一个很好计算,就是设 \(g(i)\) 表示 \(\gcd\)\(i\) 的倍数的方案数,显然若设 \(cnt[i]\) 表示 \(i\) 的倍数的个数,那么就有 \(g(i) = \binom{cnt[i]}{k}\)
也可以显然发现以下这个式子:

\[g(d) = \sum_{d|i} f(i)
\]

那么个根据莫比乌斯反演,就有:

\[\begin{aligned}
f(i)
&= \sum_{i|d} g(d) \mu(\dfrac{d}{i}) \\
&= \sum_{i|d} \binom{cnt[d]}{k} \mu(\dfrac{d}{i}) \\
\end{aligned}
\]

那么我们的答案也就可以写为:

\[\begin{aligned}
Ans
&= \sum_{i=1}^{mx} i \sum_{i | d} \mu(\dfrac{d}{i}) \binom{cnt[d]}{k} \\
&= \sum_{d=1}^{mx} \binom{cnt[d]}{k} \sum_{i | d} i \times \mu(\dfrac{d}{i})
\end{aligned}
\]

考虑我们新加入一个数也就会对前面的这个组合数造成贡献,稍微加加减减就好了。
对于后面的那一个显然这就是 \(\varphi\) 函数,因为 \(\mu * id = \varphi\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int MOD = 1e9+7;
vector<int> v[N];
int tot,phi[N],prime[N],fac[N],inv[N],cnt[N],ans;
bool flag[N];
int mod(int x){
	return ((x % MOD) + MOD)%MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
void pre_work(int n){
	for(int i=1; i<=n; i++)
		for(int j=i; j<=n; j += i)
			v[j].push_back(i);
	phi[1] = 1;
	for(int i=2; i<=n; i++){
		if(!flag[i]){
			prime[++tot] = i;
			phi[i] = i-1;
		}
		for(int j=1; j<=tot && i * prime[j] <= n; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0){
				phi[i * prime[j]] = phi[i] * prime[j];
				break;
			}
			else	phi[i * prime[j]] = phi[i] * (prime[j] - 1);
		}
	}
	fac[0] = 1;
	for(int i=1; i<=n; i++)	fac[i] = mod(fac[i-1] * i);
	inv[n] = power(fac[n],MOD-2);
	for(int i=n-1; i>=0; i--)	inv[i] = mod(inv[i+1] * (i + 1));
}
int C(int n,int m){
	if(n < m)	return 0;
	return mod(mod(fac[n] * inv[m]) * inv[n - m]);
}
void insert(int x,int k){
	for(int i : v[x]){
		int tmp = C(cnt[i],k);
		cnt[i]++;
		ans = mod(ans + phi[i] * mod(C(cnt[i],k) - tmp));
	}
}
signed main(){
	pre_work(1000000);
	int n,k,q;
	scanf("%lld%lld%lld",&n,&k,&q);
	for(int i=1; i<=n; i++){
		int x;
		scanf("%lld",&x);
		insert(x,k);
	}
	for(int i=1; i<=q; i++){
		int x;
		scanf("%lld",&x);
		insert(x,k);

		printf("%lld\n",ans);
	}
	return 0;
}

[SDOI2017]数字表格

题目分析:

直接来上式子啦:

\[\begin{aligned}
&= \prod_{i=1}^n \prod_{j=1}^m f_{\gcd(i,j)} \\
&= \prod_{d=1}^n \prod_{i=1}^{\lfloor \frac{n}{d} \rfloor} \prod_{j=1}^{\lfloor \frac{m}{d} \rfloor} [\gcd(i,j) = 1] f_{d} \\
&= \prod_{d=1}^n f_{d} ^{(\sum_{i=1}^{\lfloor \frac{n}{d} \rfloor} \sum_{j=1}^{\lfloor \frac{m}{d} \rfloor} [\gcd(i,j) = 1])}
\end{aligned}
\]

这个指数就很莫反的样子,所以就拿出来反一下:

\[\begin{aligned}
&= \sum_{i=1}^{\lfloor \frac{n}{d} \rfloor} \sum_{j=1}^{\lfloor \frac{m}{d} \rfloor} [\gcd(i,j) = 1] \\
&= \sum_{t=1} ^ {\lfloor \frac{n}{d} \rfloor} \mu(t) \sum_{i=1}^{\lfloor \frac{n}{td} \rfloor} \sum_{j=1}^{\lfloor \frac{m}{td} \rfloor} 1 \\
&= \sum_{t=1} ^ {\lfloor \frac{n}{d} \rfloor} \mu(t) \lfloor \frac{n}{td} \rfloor \lfloor \frac{m}{td} \rfloor
\end{aligned}
\]

\(T = td\),那么继续化:

\[\begin{aligned}
&= \prod_{T=1}^n \prod_{d|T} f_d^{\mu(\frac{T}{d})\lfloor \frac{n}{T} \rfloor \lfloor \frac{m}{T} \rfloor} \\
&= \prod_{T=1}^n (\prod_{d|T} f_d^{\mu(\frac{T}{d})})^{\lfloor \frac{n}{T} \rfloor \lfloor \frac{m}{T} \rfloor} \\
\end{aligned}
\]

所以对于里面的直接 \(O(n \log n)\) 预处理一下,然后再整除分块就可以实现 \(O(\sqrt{n})\) 单次查询了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int MOD = 1e9+7;
int tot,fib[N],mu[N],g[N],prime[N],F[N],inv[N],pre[N];
bool flag[N];
int mod(int x){
	return ((x%MOD)+MOD)%MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
void pre_work(int n){
	fib[0] = 0,fib[1] = 1;
	for(int i=2; i<=n; i++)	fib[i] = mod(fib[i-1] + fib[i-2]);
	mu[1] = 1;
	for(int i=2; i<=n; i++){
		if(!flag[i]){
			mu[i] = -1;
			prime[++tot] = i;
		}
		for(int j=1; j<=tot && i * prime[j] <= n; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j]	== 0)	break;
			else	mu[i * prime[j]] = -mu[i];
		}
	}
	for(int i=1; i<=n; i++)	g[i] = power(fib[i],MOD-2);
	for(int i=1; i<=n; i++)	F[i] = 1;
	for(int d=1; d<=n; d++){
		for(int T=d; T<=n; T+=d){
			int t = T / d;
			if(mu[t] == 1)	F[T] = mod(F[T] * fib[d]);
			else if(mu[t] == -1)	F[T] = mod(F[T] * g[d]);
		}
	}
	pre[0] = 1;
	for(int i=1; i<=n; i++)	pre[i] = mod(pre[i-1] * F[i]);
	for(int i=0; i<=n; i++)	inv[i] = power(pre[i],MOD-2);
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	pre_work(1000000);
	int t;
	scanf("%lld",&t);
	while(t--){
		int n,m;
		scanf("%lld%lld",&n,&m);
		if(n > m)	swap(n,m);
		int ans = 1;
		for(int l=1,r; l<=n; l = r + 1){
			r = min(n/(n/l),m/(m/l));
			ans = mod(ans * power(mod(pre[r] * inv[l-1]),(n/l) * (m/l)));
		}
		printf("%lld\n",ans);
	}
	return 0;
}

[CQOI2007]余数求和

题目分析:

显然式子可以化成:

\[\sum_{i=1}^n k - \lfloor \dfrac{k}{i} \rfloor \times i
\]

所以直接整除分块一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
	int n,k;
	scanf("%lld%lld",&n,&k);
	int ans = 0;
	for(int l=1,r; l <= n; l = r + 1){
		if(k / l != 0)	r = min(k / (k / l),n);
		else	r = n;
		ans = ans + (r - l + 1) * k - (k / l) * (l + r) * (r - l + 1) / 2;
	}
	printf("%lld\n",ans);
	return 0;
}

Luogu P4626 一道水题 II

题目分析:

题目也就是让我们求 \([1,n]\)\(lcm\),所以考虑 \(lcm\) 其实就是质因数分解之后每个质因数的质数取 \(\max\) 所以直接枚举每一个质数判断其在 \([1,n]\) 中的最高次项就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e8+5;
const int MOD = 1e8+7;
int prime[5761456],tot;
bitset<N> flag;
void pre_work(int mx){
	for(int i=2; i<=mx; i++){
		if(!flag[i]){
			prime[++tot] = i;
		}
		for(int j=1; j<=tot && 1ll * i * prime[j] <= 1ll * mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0)	break;
		}
	}
}
int main(){
//	freopen("in.txt","r",stdin);
	int n;
	scanf("%d",&n);
	pre_work(n);
	long long ans = 1;
	for(int i=1; i<=tot; i++){
		long long now = 1;
		while(1ll * now * prime[i] <= 1ll * n)	now = 1ll * now * prime[i];
		ans = (ans * now)%MOD;
	}
	printf("%lld\n",ans);
	return 0;
}

Luogu P6810 「MCOI-02」Convex Hull 凸包

题目分析:

既然看到了 \(\gcd\) 当然可以想到莫反,那就是推推推啦:

\[\begin{aligned}
&= \sum_{i=1}^n \sum_{j=1}^m \tau(i)\tau(j)\tau(\gcd(i,j)) \\
&= \sum_{d=1}^n \tau(d) \sum_{i=1}^{\lfloor \frac{n}{d} \rfloor} \sum_{j=1}^{\lfloor \frac{m}{d} \rfloor} [\gcd(i,j)=1] \tau(d\times i)\tau(d\times j) \\
&= \sum_{d=1}^n \tau(d) \sum_{t=1}^{\lfloor \frac{n}{d} \rfloor} \mu(t) \sum_{i=1}^{\lfloor \frac{n}{td} \rfloor} \sum_{j=1}^{\lfloor \frac{m}{td} \rfloor}\tau(td\times i)\tau(td\times j) \\
&= \sum_{T=1}^n \sum_{d|T} \tau(d)\mu(\frac{T}{d}) \sum_{i=1}^{\lfloor \frac{n}{T} \rfloor} \sum_{j=1}^{\lfloor \frac{m}{T} \rfloor}\tau(T\times i)\tau(T\times j)
\end{aligned}
\]

我们可以发现一个性质:\(\tau = I * I\),所以 \(\tau * \mu = I * I * \mu = I\),也就是对于 \(\tau * \mu\) 的值恒为 \(1\),所以就是前面那一坨为 \(1\),也就转化为了求:

\[\sum_{T=1}^n \bigg(\sum_{i=1}^{\lfloor \frac{n}{T} \rfloor} \tau(T\times i)\bigg)\bigg(\sum_{j=1}^{\lfloor \frac{m}{T} \rfloor}\tau(T\times j)\bigg)
\]

那么直接爆算就可以过去了

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e6+5;
int t[N],MOD;
int solve(int n,int k){
	int ans = 0;
	for(int i = k; i <= n; i += k)	
		ans = (ans + t[i])%MOD;
	return ans;
}
signed main(){
	int n,m;
	scanf("%lld%lld%lld",&n,&m,&MOD);
	if(n > m)	swap(n,m);
	for(int i=1; i<=m; i++){
		for(int j=i; j<=m; j+=i){
			t[j]++;
		}
	}
	int ans = 0;
	for(int T=1; T<=n; T++){
		ans = (ans + (solve(n,T) * solve(m,T))%MOD)%MOD;
	}
	printf("%lld\n",ans);
	return 0;
}

CF1725L Lemper Cooking Competition

题目分析:

我们可以发现:对于任意一个数进行操作都不会改变我们的权值和。
所以就可以朝着差分和前缀和的方向去想。
可以发现:这个操作相当于交换前缀和。
既然是非负也就是说前缀和是递增的,那么交换次数也就是逆序对数量。
注意最后一个不能交换,判断无解也就很简单了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
int pre[N],a[N],b[N],val[N],tot;
void modify(int x,int v){
	for(;x <= tot; x += x & (-x))	val[x] += v;
}
int query(int x){
	int ans = 0;
	for(;x;x -= x & (-x))	ans += val[x];
	return ans;
}
signed main(){
	int n;
	scanf("%lld",&n);
	for(int i=1; i<=n; i++)	scanf("%lld",&a[i]);
	for(int i=1; i<=n; i++)	pre[i] = pre[i-1] + a[i];
	bool flag = true;
	for(int i=1; i<n; i++){
		if(pre[i] < 0 || pre[i] > pre[n])
			flag = false;
	}
	if(!flag || pre[n] < 0){
		printf("-1\n");
		return 0;
	}
	for(int i=1; i<=n; i++)	b[++tot] = pre[i];
	sort(b+1,b+tot+1);
	tot = unique(b+1,b+tot+1) - b - 1;
	for(int i=1; i<=n; i++)	pre[i] = lower_bound(b+1,b+tot+1,pre[i]) - b;
	int ans = 0;
	for(int i=1; i<n; i++){
		ans += (i - 1) - query(pre[i]);
		modify(pre[i],1);
	}
	printf("%lld\n",ans);
	return 0;
}

10.18

CF1720D2 Xor-Subsequence (hard version)

题目分析:

我们可以发现对于一个子序列后面新加入一个值,会造成影响的只有最后一个数,所以我们显然可以记 \(dp[i]\) 表示以 \(i\) 结尾的最长的合法子序列的长度。
转移如果直接暴力去弄显然过不去,那么就考虑,既然是异或操作大概是和 Trie 树有点关系。
我们发现若 \(i\) 可以从 \(j\) 转移过来也就是意味着 \(a_j \oplus i < a_i \oplus j\),其实也就是按位来看,从高到低第 \(k\)\(a_j \oplus i = 0 \and a_i \oplus j = 1\),且满足前 \(k-1\) 位,\(a_j \oplus i = a_i \oplus j\),难道我们要去建一个四叉的 Trie 树去分别对应四种情况吗?
其实是可以的,但是比较麻烦,我们考虑对于相同的其实移项就可以发现他就是 \(a_j \oplus j = a_i \oplus i\),所以可以直接按 \(a_i \oplus i\) 建 Trie 树,那么统计答案怎么办呢?
可以考虑对于每一个节点维护 \(mx[now][i][j]\) 表示以 \(now\) 节点为根的子树,下一步通过第 \(k\)\(a_p = i,p = j\) 转移的最大 \(dp\)
那么就可以根据这个限制出来我们上面的条件,就直接过了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e7+5;
int rt=1,tot=1,ch[N][2],mx[N][2][2],a[N],dp[N];
void insert(int x,int val){
	int now = rt;
	for(int i=30; i>=0; i--){
		int p = (((x-1) ^ a[x]) >> i) & 1;
		if(!ch[now][p])	ch[now][p] = ++tot;
		mx[now][(a[x]>>i)&1][((x-1)>>i)&1] = max(mx[now][(a[x]>>i)&1][((x-1)>>i)&1],val);
		now = ch[now][p];
	}
}
int query(int x){
	int now = rt;
	int ans = 0;
	for(int i=30; i>=0; i--){
		int p = (((x-1) ^ a[x]) >> i) & 1;
		ans = max(ans,mx[now][(((x-1)>>i)&1)][((a[x] >> i) & 1) ^ 1]);
		now = ch[now][p];
		if(!now)	break;
	}
	return ans;
}
signed main(){
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		dp[1] = 1;
		insert(1,dp[1]);
		for(int i=2; i<=n; i++){
			dp[i] = query(i) + 1;
			insert(i,dp[i]);
		}
		int ans = 0;
		for(int i=1; i<=n; i++)	ans = max(ans,dp[i]);
		printf("%d\n",ans);
		for(int i=1; i<=tot; i++){
			for(int j=0; j<=1; j++){
				for(int k=0; k<=1; k++){
					mx[i][j][k] = 0;
				}
				ch[i][j] = 0;
			}
		}
		tot = 1;
	}
	return 0;
}

CF1718C Tonya and Burenka-179

题目分析:

有一个经典的结论,一个长度为 \(n\) 的环,每步走 \(k\) 的大小,经过的节点个数一定是 \(\dfrac{n}{\gcd(n,k)}\) 个,且换上每个点一定模 \(\gcd(n,k)\) 同余,也就是每个节点经过 \(\gcd(n,k)\)

那么对于一个二元组 \((s,k)\) 的贡献就是

\[\gcd(n,k) \sum_{i = s \mod \gcd(n,k)} a_i
\]

一个非常朴素的想法:我们一开始预处理出来所有 \((s,k)\) 的最优答案,然后每次修改就去更新所有包含他的个数。
但是发现这样其实我们 \(k\) 的范围是约数个数,数量不少,大概率过不去。
我们也可以发现另一个性质:不需要枚举所有的约数,只需要质因子就可以了。
所以复杂度就降到了质因子个数,就可以过了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
int a[N],f[10][N],d[10];
multiset<int> st;
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%lld",&t);
	while(t--){
		int n,q;
		scanf("%lld%lld",&n,&q);
		for(int i=1; i<=n; i++)	scanf("%lld",&a[i]);
		int tmp = n,tot = 0;
		for(int i=2; i<=tmp; i++){
			if(tmp % i == 0){
				d[++tot] = n / i;    // n / (n / i) = i 是个质数
				while(tmp % i == 0)	tmp /= i; 
			}
		}
		for(int i=1; i<=tot; i++){
			for(int j=0; j<d[i]; j++)	f[i][j] = 0;
			for(int j=1; j<=n; j++)		f[i][j % d[i]] += a[j];
			for(int j=0; j<d[i]; j++)	st.insert(d[i] * f[i][j]);
		}
		printf("%lld\n",*(st.rbegin()));
		while(q--){
			int x,y;
			scanf("%lld%lld",&x,&y);
			for(int i=1; i<=tot; i++){
				st.erase(st.find(d[i] * f[i][x % d[i]]));
				f[i][x % d[i]] += y - a[x];
				st.insert(d[i] * f[i][x % d[i]]);
			}
			a[x] = y;
			printf("%lld\n",*(st.rbegin()));
		}
		st.clear();
	}
	return 0;
}

CF1715E Long Way Home

题目分析:

考虑我们最短路肯定是类似 走 - 飞 - 走 - 飞 -,所以其实可以考虑分开处理。
处理一次走然后处理一次飞,交替着来。
对于走显然直接跑最短路算法就可以,而对于飞,其实是满足下列这个式子的转移:

\[dis[i] = \min\{ dis[j] + (i - j)^2 \}
\]

稍微化简一下:

\[dis[i] = dis[j] + i^2 + j^2 - 2\times i \times j
\]

也就是非常经典的斜率优化了。
注意这里的 \(j\) 不限制小于 \(i\),所以需要将凸包整体建出来,然后通过凸包上二分得到最优的解。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int> 
using namespace std;
const int N = 1e5+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[2 * N];
int cnt,n,m,k,head[N],dis[N],st[N],tmp[N];
bool vis[N];
int X(int i){return 2 * i;}
int Y(int i){return dis[i] + 1ll * i * i;}
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
}
void dij(){
	priority_queue<PII> q;
	for(int i=1; i<=n; i++)	q.push({-dis[i],i});
	for(int i=1; i<=n; i++)	vis[i] = false;
	while(!q.empty()){
		int now = q.top().second;q.pop();
		if(vis[now])	continue;
		vis[now] = true;
		for(int i = head[now]; i;i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] > dis[now] + e[i].val){
				dis[to] = dis[now] + e[i].val;
				q.push({-dis[to],to});
			}
		}
	}
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld%lld",&n,&m,&k);
	for(int i=1; i<=n; i++)	dis[i] = 1000000000000;
	for(int i=1; i<=m; i++){
		int u,v,w;
		scanf("%lld%lld%lld",&u,&v,&w);
		add_edge(u,v,w);add_edge(v,u,w);
	}
	dis[1] = 0;
	dij();
//	for(int i=1; i<=n; i++)	printf("dis[%lld] = %lld\n",i,dis[i]);
	while(k--){
		//建凸包
		int top = 0;
		for(int i=1; i<=n; i++){  //这样的话就需要建下凸包
			while(top >= 2 && (double)(Y(i) - Y(st[top])) / (X(i) - X(st[top])) <= (double)(Y(st[top]) - Y(st[top - 1])) / (X(st[top]) - X(st[top-1])))	top--;
			st[++top] = i;
		} 
//		for(int i=1; i<=top; i++)	printf("%lld %lld\n",X(st[i]),Y(st[i]));
//		printf("\n\n");
		for(int i=1; i<=n; i++){
			int l = 1,r = top,ans = 0;  //凸包上二分找最优决策点 
			while(l <= r){
				int mid = (l + r)>>1;
				if((Y(st[mid+1]) - Y(st[mid])) < 1ll * i * (X(st[mid+1]) - X(st[mid])))	l = mid + 1;
				else	r = mid - 1;
			}
			ans = l;
//			printf("%lld(%lld,%lld) -> %lld\n",st[ans],X(st[ans]),Y(st[ans]),i);
			tmp[i] = i * i + Y(st[ans]) - i * X(st[ans]);
		}
		for(int i=1; i<=n; i++) dis[i] = tmp[i];
//		for(int i=1; i<=n; i++)	printf("dis[%lld] = %lld\n",i,dis[i]);
		dij();
	}
	for(int i=1; i<=n; i++){
		printf("%lld ",dis[i]);
	}
	return 0;
}

10.19

CF1713E Cross Swapping

题目分析:

我们可以发现矩阵的右上角的元素与左上角一一对应。
所以其实我们只要扫一遍右上角的,确定他们的状态就好了。
我们考虑挨个考虑:
若对于 \(a_{i,j}\) 显然,其想要换的条件就是 \(a_{j,i} < a_{i,j}\),而换就意味着 \(k=i\)\(k=j\) 操作一次。
而对于 \(a_{i,j}\) 如果不换,也就是意味着 \(k=i\)\(k=j\) 都不进行操作。
其实也就是涉及两个操作之间的关系,考虑带权并查集维护。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3+5;
int val[N],fa[N],a[N][N];
int find(int x){
	if(fa[x] == x)	return x;
	int to = find(fa[x]);
	val[x] ^= val[fa[x]];
	return fa[x] = to;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n; i++){
			for(int j=1; j<=n; j++){
				scanf("%d",&a[i][j]);
			}
		}
		for(int i=1; i<=n; i++)	fa[i] = i,val[i] = 0;
		for(int i=1; i<=n; i++){
			for(int j=i+1; j<=n; j++){
				if(a[i][j] != a[j][i]){
					int fx = find(i),fy = find(j);
					if(a[i][j] > a[j][i]){  //想换 
						if(fx != fy){
							fa[fx] = fy;val[fx] = val[i] ^ val[j] ^ 1;
							swap(a[i][j],a[j][i]);
						}
						else if(val[i] ^ val[j])	swap(a[i][j],a[j][i]);
					}
					else if(a[i][j] < a[j][i]){
						if(fx != fy){
							fa[fx] = fy;val[fx] = val[i] ^ val[j];
						}
						else if(val[i] ^ val[j])	swap(a[i][j],a[j][i]);
					}
				}
			}
		}
		for(int i=1; i<=n; i++){
			for(int j=1; j<=n; j++){
				printf("%d ",a[i][j]);
			}
			printf("\n");
		}
	}
	return 0;
}

10.20

CF1712E2 LCM Sum (hard version)

题目分析:

可以考虑转化一下求解 \(lcm(i,j,k) < i + j + k\) 的方案数。
首先我们可以发现 \(i < j < k\),所以 \(lcm(i,j,k) < 3 \times k\)
根据 \(lcm\) 的定义,所以 \(k\)\(lcm(i,j,k)\) 的约数,所以其实 \(lcm\) 只有两种取值:\(k\)\(2\times k\)
可以通过一系列神奇的操作发现,满足 \(lcm(i,j,k) = 2k\) 的只有 \((3,4,6)\)\((6,10,15)\) 及其整数倍,所以这一部分可以很快处理出来。
而对于 \(k\) 的情况,我们可以发现如果要对答案产生贡献,必须使得 \(i,k\) 符合要求,所以可以考虑将 \(i,k\) 相同的放在一起处理。
其实我们要满足 \(lcm(i,j,k) = k\),其实就是要满足 \(i,j\)\(k\) 的约数,所以直接暴力枚举可以通过 E1。
而显然我们可以考虑枚举 \(k\),对于每一个 \(i\) 维护一个合法的答案的值,然后直接求个和就好了。
其实也就是一个类似的二维数点。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAX = 2e5;
const int N = 2e5 + 5;
struct Query{
	int l,r,opt;
}q[N];
int ans[N],sum[MAX + 5];
vector<int> v[MAX + 5];
void modify(int x,int v){
	for(;x <= MAX; x += x & (-x))	sum[x] += v;
}
int query(int x){
	int ans = 0;
	for(;x;x -= x & (-x))	ans += sum[x];
	return ans;
}
bool cmp(Query a,Query b){
	return a.r < b.r;
}
signed main(){
	int t;
	scanf("%lld",&t);
	for(int i=1; i<=t; i++){
		scanf("%lld%lld",&q[i].l,&q[i].r);
		int l= q[i].l,r = q[i].r;
		ans[i] -= max(0ll,r/6 - (l-1)/3);
		ans[i] -= max(0ll,r/15 - (l-1)/6);
		q[i].opt = i;
	}
	sort(q+1,q+t+1,cmp);
	for(int i=1; i<=MAX; i++){
		for(int j = 2*i; j<=MAX; j+=i){
			v[j].push_back(i);
		}
	}
	int now = 1;
	for(int i=1; i<=MAX; i++){
		for(int j =0; j<v[i].size(); j++)	modify(v[i][j],v[i].size() - j - 1);
		while(now <= t && q[now].r <= i)	ans[q[now].opt] -= query(q[now].r) - query(q[now].l - 1),now++;
	}
	for(int i=1; i<=t; i++){
		int l = q[i].l,r = q[i].r;
		ans[q[i].opt] += (r-l-1) * (r-l) * (r-l+1) / 6;
	}
	for(int i=1; i<=t; i++)		printf("%lld\n",ans[i]);
	return 0;
}

10.21

1709E XOR Tree

题目分析:

我们可以显然将贡献拆开:
也就设 \(dis[i]\) 表示 \(i\) 到根的路径上的点权的异或值,那么对于路径 \((i,j)\) 的权值就是 \(dis[i] \oplus dis[j] \oplus a[lca(i,j)]\)
我们可以显然考虑递归处理,因为我们可以发现我们需要从叶子开始一点点处理,这样才能保证一定是最优的。
那么对于一个子树内其实就是求多少条路径满足上述的式子等于 \(0\),只不过这个时候 \(lca\) 就是根,那么开个桶记一下就好了。
显然我们通过将 \(a\) 改成一些特别神奇的值,使得这个子树内的链不可能与其他的路径组成新的符合条件的路径,所以一旦更改了就直接把这个子树扔了就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
int cnt,head[N],dis[N],a[N],ans;
multiset<int> st[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void dfs(int now,int fath){
	bool flag = false;
	dis[now] = dis[fath] ^ a[now];
	st[now].insert(dis[now]);
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs(to,now);
		if(st[now].size() < st[to].size())	swap(st[now],st[to]);
		for(int j : st[to])	if(st[now].find(j ^ a[now]) != st[now].end())	flag = true;
		for(int j : st[to])	st[now].insert(j);
	}
	if(flag){
		ans++;
		st[now].clear();
	}
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
	for(int i=1; i<n; i++){
		int u,v;
		scanf("%d%d",&u,&v);
		add_edge(u,v);add_edge(v,u);
	}
	dfs(1,0);
	printf("%d\n",ans);
	return 0;
}

1707C DFS Trees

题目分析:

可以发现最小生成树是唯一的,因为各个边的权值都不一样。
所以其实就判断一下最后找到的树是否是这个最小生成树就好了。
我们可以发现代码其实就是一个 dfs,而 dfs 是没有横叉边的,当我们 dfs 到的树边都是最小生成树的边时其实就是意味着我们剩下的边都是返祖边。
其实这样就会发现:若节点 \(i\) 合法,当且仅当通过 \(i\) 得到的 dfs 树上,我们不在最小生成树上的边都是祖先后代关系。
我们可以转化一下,考虑对于每一条边哪些点是符合条件的,就好判断多了。
显然我们可以随意找一个点作为根,然后对于每一条非树边的两个端点的子树内的点作为根时,他们是祖先后代关系,所以就很好做了。
需要特判一下,以当前选择的点为根他们就已经是祖先后代关系的情况。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 6e5+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
int cnt,n,m,head[N],val[N],fa[N][30],from[N],to[N],dep[N],f[N];
bool flag[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void calc(int now,int fath){
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		val[to] += val[now];
		calc(to,now);
	}
}
void dfs(int now,int fath){
	fa[now][0] = fath;dep[now] = dep[fath] + 1;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs(to,now);	
	}
}
void pre_lca(){
	for(int i=1; i<=24; i++){
		for(int j=1; j<=n; j++){
			fa[j][i] = fa[fa[j][i-1]][i-1];
		}
	}
}
int find(int x){
	if(f[x] == x)	return x;
	return f[x] = find(f[x]);
}
int get_lca(int x,int y){
	if(dep[x] > dep[y])	swap(x,y);
	for(int i=24; i>=0; i--){
		if(dep[fa[y][i]] >= dep[x])
			y = fa[y][i];
	}	
	if(x == y)	return x;
	for(int i=24; i>=0 ;i--){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];y = fa[y][i];
		}
	}
	return fa[x][0];
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout); 
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++)	scanf("%d%d",&from[i],&to[i]);	
	for(int i=1; i<=n; i++)	f[i] = i;
	for(int i=1; i<=m; i++){
		if(find(from[i]) != find(to[i])){
			f[find(from[i])] = find(to[i]);
			flag[i] = true;
			add_edge(from[i],to[i]);add_edge(to[i],from[i]);
//			printf("%d %d\n",from[i],to[i]);
		}
	}
//	for(int i=1; i<=m; i++){
//		printf("%d",flag[i]);
//	}
//	printf("\n");
	dep[1] = 1;
	dfs(1,0);
	pre_lca();
	for(int i=1; i<=m; i++){
		if(flag[i])	continue;
		int u = from[i],v = to[i];
		if(dep[u] > dep[v])	swap(u,v);
		int lca = get_lca(u,v);
//		printf("lca(%d,%d) = %d\n",u,v,lca);
		if(lca == u){
			val[1]++;val[v]++;
			for(int j=24; j>=0; j--){
				if(dep[fa[v][j]] > dep[lca])
					v = fa[v][j];
			}
			val[v]--;
		}
		else	val[from[i]]++,val[to[i]]++;
	}
//	for(int i=1; i<=n; i++){
//		printf("%d ",val[i]);
//	}
//	printf("\n");
	calc(1,0);
	for(int i=1; i<=n; i++){
		if(val[i] == m - (n - 1))	printf("1");
		else	printf("0");
	}
	return 0;
}

10.22

CF1706E Qpwoeirut and Vertices

题目分析:

我们可以发现如果将每条边的时间戳作为它的权值,那么我们只要求一个 Kruskal 重构树,那么对于任意两个点 \(x,y\) 他们在联通的时间就是他们在树上的 lca 的点权。
那么我们考虑一段区间怎么搞?拆!
拆成 \([l,l+1],[l+1,l+2],\cdots,[r-1,r]\),也就是维护一下 \([i,i+1]\) 的联通情况,那么对于一次询问就是一个区间最大值查询,用一个 st 表维护下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N];
int cnt,head[N],f[N],fa[N][27],dep[N],st[N][27],val[N],from[N],to[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void dfs(int now,int fath){
	fa[now][0] = fath;dep[now] = dep[fath] + 1;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs(to,now);
	}
}
void pre_lca(int n){
	for(int i=1; i<=24; i++){
		for(int j=1; j<=n; j++){
			fa[j][i] = fa[fa[j][i-1]][i-1];
		}
	}
}
int lca(int x,int y){
	if(dep[x] > dep[y])	swap(x,y);
	for(int i=24; i>=0; i--){
		if(dep[fa[y][i]] >= dep[x])
			y = fa[y][i];
	}
	if(x == y)	return x;
	for(int i=24; i>=0; i--){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}
int get_mx(int l,int r){
	if(l > r)	return 0;
	int len = log2(r-l+1);
	return max(st[l][len],st[r-(1<<len)+1][len]);
}
int find(int x){
	if(f[x] == x)	return x;
	return f[x] = find(f[x]);
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		int n,m,q;
		scanf("%d%d%d",&n,&m,&q);
		for(int i=1; i<=m; i++){
			scanf("%d%d",&from[i],&to[i]);
		}
		for(int i=1; i<=n * 2; i++)	f[i] = i;
		int tot = n;
		for(int i=1; i<=m; i++){
			if(find(from[i]) != find(to[i])){
				int u = find(from[i]),v = find(to[i]);
				f[u] = f[v] = ++tot;val[tot] = i;
				add_edge(tot,u);add_edge(tot,v);
			}
		}
		dep[tot] = 1;
		dfs(tot,0);
		pre_lca(tot);
		for(int i=1; i<n; i++)	st[i][0] = val[lca(i,i+1)];
		for(int i=1; i<=24; i++){
			for(int j=1; j + (1<<i) <= n; j++){
				st[j][i] = max(st[j][i-1],st[j + (1<<(i-1))][i-1]);
			}
		}
		for(int i=1; i<=q; i++){
			int l,r;
			scanf("%d%d",&l,&r);
			if(l == r)	printf("0 ");
			else	printf("%d ",get_mx(l,r-1));
		}
		printf("\n");
		for(int i=1; i<=24; i++){
			for(int j=1; j<=n; j++){
				st[j][i] = 0;
			}
		}
		cnt = 0;
		for(int i=1; i<=tot; i++)	head[i] = 0;
	}
	return 0;
}

CF1706D2 Chopping Carrots (Hard Version)

题目分析:

我们可以枚举最小值 \(v\),那么对于一个数最优的 \(p\) 显然就是 \(\lfloor \dfrac{a}{v} \rfloor\)
所以直接去暴力弄的话大概可以过 D1。
我们可以考虑记 \(max(v)\) 表示以 \(v\) 为最小值的时候的最大值。
根据整除分块的理论,每一个数的可能出现的除之后的只有 \(O(\sqrt{a})\) 个,我们不妨假设这些数为 \(s_1,s_2,\cdots,s_k\)
我们可以发现当 \(s_i < v \le s_{i+1}\),我们 \(max(v)\) 至少要是 \(s_{i+1}\)(此时不要考虑上面那个 D1 的做法)所以直接用 \(s_{i+1}\) 更新一下 \(s_{i} + 1\),然后扫一遍统计一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6+5;
const int INF = 1e9+6;
int a[N],ans[N]; 
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		memset(ans,0,sizeof(ans));
		int n,k;
		scanf("%d%d",&n,&k);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++){
			int pre = -1;
			for(int l=1,r; l<=min(a[i],k); l = r + 1){
				r = a[i] / (a[i] / l);
				int cur = a[i] / l;
				ans[cur + 1] = max(ans[cur + 1],pre);
				pre = cur;
			}
			ans[0] = max(ans[0],pre);
		}
		int mx = 0;
		int res = INF;
		for(int i=0; i<=a[1]; i++){
			mx = max(mx,ans[i]);
			res = min(res,mx - i);
		}
		printf("%d\n",res);
	}
	return 0;
}

10.23

1705E Mark and Professor Koro

题目分析:

我们可以考虑直接维护操作之后的数列。
建立值域线段树之后,可以显然发现每个位置只可能是 \(1\)\(0\)
那么就来分析下面的一个个操作了。

  • 查询:就是寻找最靠右的 \(1\) 的位置
  • 修改:
    • 删除:寻找 \(a_x\) 右边第一个 \(1\) 的位置,单点置为 \(0\),将这一段区间赋值为 \(1\),可以理解为退位了。需要特判这个位置就是 \(a_x\)
    • 加入:寻找 \(y\) 右边第一个 \(0\) 的位置,单点置为 \(1\),将这一段区间赋值为 \(0\),可以理解为进位了。需要特判这个位置就是 \(y\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAX = 2e5+50;
const int N = 4e5+5;
int a[N],cnt[MAX + 10],sum[4 * MAX][2],tag[4 * MAX],p[4 * MAX][2];
void pushdown(int now,int now_l,int now_r){
	if(tag[now] != -1){
		sum[now<<1][tag[now]^1] = sum[now<<1|1][tag[now]^1] = 0;
		int mid = (now_l + now_r)>>1;
		sum[now<<1][tag[now]] = (mid - now_l + 1);
		sum[now<<1|1][tag[now]] = (now_r - mid);
		tag[now<<1] = tag[now<<1|1] = tag[now];
		p[now<<1][tag[now]] = mid;p[now<<1|1][tag[now]] = now_r;
		p[now<<1][tag[now]^1] = -1;p[now<<1|1][tag[now]^1] = -1;
		tag[now] = -1;
	}
}
void pushup(int now){
	sum[now][1] = sum[now<<1][1] + sum[now<<1|1][1];
	sum[now][0] = sum[now<<1][0] + sum[now<<1|1][0];
	p[now][1] = max(p[now<<1][1],p[now<<1|1][1]);
	p[now][0] = max(p[now<<1][0],p[now<<1|1][0]);
}
int get_pos(int now,int now_l,int now_r,int pos,int v){
	if(now_l == now_r)	return now_l;
	pushdown(now,now_l,now_r);
	int mid = (now_l + now_r)>>1;
	int ans = 0;
	if(p[now<<1][v] >= pos)	ans = get_pos(now<<1,now_l,mid,pos,v);
	else	ans = get_pos(now<<1|1,mid+1,now_r,pos,v);
	pushup(now);
	return ans;
}
int get_ans(int now,int now_l,int now_r){
	if(now_l == now_r)	return now_l;
	pushdown(now,now_l,now_r);
	int mid = (now_l + now_r)>>1;
	int ans = 0;
	if(sum[now<<1|1][1])	ans = get_ans(now<<1|1,mid+1,now_r);
	else	ans = get_ans(now<<1,now_l,mid);
	pushup(now);
	return ans;	
}
void modify(int now,int now_l,int now_r,int l,int r,int val){
	if(l <= now_l && r >= now_r){
		tag[now] = val;
		sum[now][val^1] = 0;
		sum[now][val] = (now_r - now_l + 1);
		p[now][val] = now_r;p[now][val^1] = -1;
		return;
	}
	pushdown(now,now_l,now_r);
	int mid = (now_l + now_r)>>1;
	if(l <= mid)	modify(now<<1,now_l,mid,l,r,val);
	if(r > mid)		modify(now<<1|1,mid+1,now_r,l,r,val);
	pushup(now);
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	memset(tag,-1,sizeof(tag));memset(p,-1,sizeof(p));
	int n,q;
	scanf("%d%d",&n,&q);
	for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
	for(int i=1; i<=n; i++)	cnt[a[i]]++;
	for(int i=1; i<=MAX; i++)	cnt[i+1] += cnt[i] / 2,cnt[i] %= 2;
	for(int i=1; i<=MAX; i++)	modify(1,1,MAX,i,i,cnt[i]);
	for(int i=1; i<=q; i++){
		int x,y;
		scanf("%d%d",&x,&y);
		//考虑删除
		int pos1 = get_pos(1,1,MAX,a[x],1);
		if(pos1 == a[x])	modify(1,1,MAX,a[x],a[x],0);
		else	modify(1,1,MAX,a[x],pos1-1,1),modify(1,1,MAX,pos1,pos1,0);
		
		//考虑加入 
		a[x] = y;
		int pos0 = get_pos(1,1,MAX,a[x],0);
		if(pos0 == a[x])	modify(1,1,MAX,a[x],a[x],1);
		else	modify(1,1,MAX,a[x],pos0 - 1,0),modify(1,1,MAX,pos0,pos0,1);
		
		//考虑答案;
		printf("%d\n",get_ans(1,1,MAX)); 
	}
	return 0;
}

CF1699D Almost Triple Deletions

题目分析:

我们可以发现如果一段区间可以被完全消掉,当且仅当这个区间不存在绝对众数,并且区间长度为偶数。
所以就可以根据这个性质干很多事了。
我本来想的是直接枚举最后的答案是哪个数,但是其实完全没有必要。
我们可以设 \(f[i]\) 表示前 \(i\) 个数已经选择完了,以 \(a_i\) 为剩余的元素的长度的最大值。
转移显然就是枚举 \(j\),但是需要满足 \(a_j = a_i\)\([j+1,i-1]\) 可以被完全消除。
统计答案就是枚举每一个 \(f[i]\) 判断一下 \([i+1,n]\) 是否可以被完全消除就可以了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
const int INF = 1e9+5;
int a[N],b[N],cnt[N],f[N];
bool calc(int l,int r,int mx){
	if(l > r)	return true;
	int len = (r - l + 1);
	if(mx <= len / 2 && len % 2 == 0)	return true;
	return false;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		int tot = 0;
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		for(int i=1; i<=n; i++)	b[++tot] = a[i];
		sort(b+1,b+tot+1);
		tot = unique(b+1,b+tot+1) - b - 1;
		for(int i=1; i<=n; i++)	a[i] = lower_bound(b+1,b+tot+1,a[i]) - b;
		for(int i=1; i<=n; i++){
			for(int k=1; k<=tot; k++)	cnt[k] = 0;
			f[i] = -INF;
			int mx = 0;
			for(int j=i-1; j>=0; j--){
				if(j != i-1)	cnt[a[j+1]] ++,mx = max(mx,cnt[a[j+1]]);
				if(calc(j+1,i-1,mx) && (a[j] == a[i] || j == 0))
					f[i] = max(f[i],f[j]+1);
			}
		}
//		for(int i=1; i<=n; i++)	printf("f[%d] = %d\n",i,f[i]);
		int ans = 0,mx = 0;
		for(int i=1; i<=tot; i++)	cnt[i] = 0;
		for(int i=n; i>=1; i--){
			if(i != n)	cnt[a[i+1]]++,mx = max(mx,cnt[a[i+1]]);
			if(calc(i+1,n,mx))	
				ans = max(ans,f[i]);
		}
		printf("%d\n",ans);
		for(int i=1; i<=n; i++)	f[i] = 0;
	}
	return 0;
}

10.24

CF1698E PermutationForces II

题目分析:

考虑如果我们已知 \(b\) 数组,如果快速地判断 \(a\) 是否可以变成 \(b\)
我们可以感受得到,当我们进行第 \(i\) 次操作的时候前 \(i-1\) 个数必然已经回到其本来的位置上了。
我们假设让 \(a\) 随着 \(b\) 排序的话,我们最终的目标其实就是让 \(a\) 变成一个顺序的排列。
可以发现这样的要求其实就是排序后: \(a_i \le i + s\),因为只有这样我们才可以保证最后能将 \(i\) 换回来。
那么如果不排序的话其实约束条件就是 \(a_i \le b_i + s\)
那么对于 \(b\) 来说其实就是 \(b_i \ge a_i - s\),那么可以直接去找每个 \(b\) 能符合条件的数的数量,按约数的紧/松排一下序就好了。
需要注意如果有值的序列便不合法,即 \(a_i - b_i > s\),那么直接判 \(0\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int> 
using namespace std;
const int N = 2e5+5;
const int MOD = 998244353;
PII a[N];
bool flag[N];
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%lld",&t);
	while(t--){
		int n,s;
		scanf("%lld%lld",&n,&s);
		for(int i=0; i<=n+5; i++)	flag[i] = false;
		for(int i=1; i<=n; i++)	scanf("%lld",&a[i].second);
		int mx = 0;
		for(int i=1; i<=n; i++){
			scanf("%lld",&a[i].first);
			if(a[i].first != -1){
				flag[a[i].first] = true;
				mx = max(mx,a[i].second - a[i].first);   //可以化成 a[i] - b[i] <= s 
			}
		}
		if(mx > s){
			printf("0\n");
			continue;
		}
		vector<int> t,c;t.clear();c.clear();
		for(int i=1; i<=n; i++)	if(!flag[i])	t.push_back(i);
		for(int i=1; i<=n; i++){
			if(a[i].first == -1){
				c.push_back(t.end() - lower_bound(t.begin(),t.end(),a[i].second - s));
			}
		}
		sort(c.begin(),c.end());
		int ans = 1;
		for(int i=0; i<c.size(); i++)	ans = ((ans * (c[i] - i))%MOD + MOD)%MOD;
		printf("%lld\n",ans);
	}
	return 0;
}

CF1697E Coloring

题目分析:

我们首先定义一个点集是合法的,当且仅当这个点集内的点可以被染成同样的颜色。
首先这一个个合法的点集之间一定不相交,因为如果相交那么对于相交的那个点来说就是会产生矛盾。
其实点集内部两两距离相等是一个很强的条件,我们手玩一下就可以发现,最多构造出一个大小为 \(4\) 的合法点集。
那么这就启示我们可以直接将原序列划分为一个个点集,然后直接枚举大小为 \(2,3,4\) 的点集的个数,剩下的全部当作大小为 \(1\) 的点集就好了。
\(a_2,a_3,a_4\) 分别代表选择的大小为 \(2,3,4\) 的点集的个数,\(cnt_2,cnt_3,cnt_4\) 代表大小为 \(2,3,4\) 的点集的总个数,\(a_1 = n - a_2\times 2 - a_3 \times 3 - a_4 \times 4\) 也就是剩下的点的个数,那么对于一种选择方案答案显然就是:

\[\binom{cnt_2}{a_2}\binom{cnt_3}{a_3}\binom{cnt_4}{a_4}\binom{n}{a_1+a_2 + a_3 + a_4} (a_1 + a_2 + a_3 + a_4)!
\]

直接枚举算的话复杂度就是 \(O(n^3)\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 105;
const int INF = 1e9+5;
const int MOD = 998244353;
struct node{
	int x,y;
}a[N];
int fac[N],inv[N],dis[N][N],g[N][10],cnt[N],mn[N],tot[10];
bool vis[N];
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
int C(int n,int m){
	return mod(mod(fac[n] * inv[m]) * inv[n - m]);
}
 main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%lld",&n);
	for(int i=1; i<=n; i++){
		scanf("%lld%lld",&a[i].x,&a[i].y);
	}
	//预处理需要用到的东西
	fac[0] = 1;
	for(int i=1; i<=n; i++)	fac[i] = mod(fac[i-1] * i);
	inv[n] = power(fac[n],MOD-2);
	for(int i=n-1; i>=0; i--)	inv[i] = mod(inv[i+1] * (i+1)); 
	//获取两点之间的距离 
	for(int i=1; i<=n; i++){
		for(int j=1; j<=n; j++){
			dis[i][j] = abs(a[i].x - a[j].x) + abs(a[i].y - a[j].y);
		}
	}
	//对于每个点求他的最小距离
	for(int i=1; i<=n; i++){
		mn[i] = INF;
		for(int j=1; j<=n; j++){
			if(i == j)	continue;
			mn[i] = min(mn[i],dis[i][j]);
		}
	} 
	//对于每一个点求最小距离点的点集
	for(int i=1; i<=n; i++){
		for(int j=1; j<=n; j++){
			if(i == j) continue;
			if(dis[i][j] == mn[i]){
				++cnt[i];if(cnt[i] >= 4)	continue;
				g[i][cnt[i]] = j;
			}
		}
	} 
	//求解有多少个大小为 2,3,4 的点集
	for(int i=1; i<=n; i++){
		if(cnt[i] >= 4 || vis[i])	continue;
		//cnt 不计算 i,所以如果超过 4 个显然不合法的
		bool flag = true;
		for(int j=1; j<=cnt[i]; j++){
			int to1 = g[i][j];
			if(mn[to1] != mn[i] || cnt[to1] != cnt[i])	flag = false;
			for(int k=1; k<=cnt[to1]; k++){
				int to2 = g[to1][k];
				if(to2 != i && dis[i][to2] != mn[i])	flag = false;
			}
		}
		if(flag){
			tot[cnt[i] + 1]++;
			vis[i] = true;
			for(int j=1; j<=cnt[i]; j++)	vis[g[i][j]] = true;
		}
	} 
	//暴力枚举 2,3,4 的个数并统计答案
	int ans = 0;
	for(int a2 = 0; a2<=tot[2]; a2++){
		for(int a3=0; a3<=tot[3]; a3++){
			for(int a4=0; a4<=tot[4]; a4++){
				int a1 = n - a2 * 2 - a3 * 3 - a4 * 4;  //点集大小为 1 的点集个数
				ans = mod(ans + mod(mod(mod(mod(C(tot[2],a2) * C(tot[3],a3)) * C(tot[4],a4)) * C(n,a1 + a2 + a3 + a4)) * fac[a1 + a2 + a3 + a4]));
			}
		}
	} 
	printf("%lld\n",ans);
	return 0;
}

CF1753A2 Make Nonzero Sum (hard version)

题目分析:

其实我们可以使用若干长度为 \(1,2\) 的区间来代替长度为 \(n(n > 2)\) 的区间。
所以其实我们可以考虑先将所有的区间都划分为长度为 \(1\),然后再考虑划分为长度为 \(2\) 就好了。
我们发现划分为长度为 \(2\) 其实就是让第二个数的正负改变,我们可以结合全部为 \(1\) 时的区间和的正负来决定我们是将正数取负还是负数取正。
这样就过了,发现其实 \(0\) 没啥用。

代码;

点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
using namespace std;
const int N = 5e5+5;
int a[N];
bool flag[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		for(int i=1; i<=n + 5; i++)	flag[i] = false;
		flag[0] = true;
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		int sum = 0;
		for(int i=1; i<=n; i++)	sum += a[i];	
		if(sum > 0){  //正的取负 
			for(int i=1; i<=n; i++){
				if(sum == 0)	break;
				if(a[i] > 0 && !flag[i-1])	flag[i] = 1,sum -= 2;
			}
		}
		else if(sum < 0){  //负的取正 
			for(int i=1; i<=n; i++){
				if(sum == 0)	break;
				if(a[i] < 0 && !flag[i-1])	flag[i] = 1,sum += 2;
			}
		}
		if(sum != 0)	printf("-1\n");
		else{
			vector<PII> v;
			for(int i=1; i<=n; i++){
				if(flag[i + 1])	v.push_back({i,i+1}),i++;
				else	v.push_back({i,i});
			}
			printf("%d\n",v.size());
			for(auto i : v)	printf("%d %d\n",i.first,i.second);
		}
	}
	return 0;
}

CF1753B Factorial Divisibility

题目分析:

题目给出的其实就是阶乘和是 \(x!\) 的倍数,其实也就是说阶乘和可以表示为 \(a \times x!\) 的形式。
我们考虑只有 \(i+1\)\(i!\) 才可以合出来一个 \((i+1)!\),且仅有这一条途径,我们就可以从低到高一点点合,最后如果剩下的全部是 \(x!\) 那么就是合法的了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 7e5+5;
int cnt[N];
int main(){
	int n,x;
	scanf("%d%d",&n,&x);
	for(int i=1; i<=n; i++){
		int a;
		scanf("%d",&a);
		cnt[a]++;
	}
	for(int i=1; i<x; i++){
		cnt[i+1] += cnt[i] / (i + 1);
		cnt[i] %= (i + 1);
		if(cnt[i]){
			printf("No\n");
			return 0;
		}
	}
	printf("Yes\n");
	return 0;
}

CF1753C Wish I Knew How to Sort

题目分析:

我们设 \(cnt\) 表示所有的 \(0\) 的数量。
那么显然可以设 \(dp[i]\) 表示前 \(cnt\) 个位置有 \(i\)\(0\) 的期望操作次数。
那么转移到 \(i+1\) 就是判断一下操作多少次就好了,其实也可以转化为 \(\dfrac{1}{概率}\),这就很好弄了,概率显然是 \(\dfrac{(cnt-i)(cnt-i)}{\frac{n\times(n-1)}{2}}\)
也就是说用前 \(cnt\) 个位置里面的 \(1\) 替换其他位置里面的 \(0\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int MOD = 998244353;
int a[N],f[N];
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t ;
	scanf("%lld",&t);
	while(t--){
		int n;
		scanf("%lld",&n);
		for(int i=1; i<=n; i++)	scanf("%lld",&a[i]);
		int cnt1=0,cnt2=0;
		for(int i=1; i<=n; i++)	cnt1 += a[i] == 0;
		for(int i=1; i<=cnt1; i++)	cnt2 += a[i] == 0;
		int inv = power(mod(n * (n - 1) / 2),MOD-2);
		for(int i=cnt2+1; i<=cnt1; i++){
			int tmp = mod((cnt1 - i + 1) * (cnt1 - i + 1));
			f[i] = mod(f[i-1] + power(mod(tmp * inv),MOD-2));
		}
		printf("%lld\n",f[cnt1]);
		for(int i=0; i<=cnt1; i++)	f[i] = 0;
	}
	return 0;
}

10.25

CF1693C Keshi in Search of AmShZ

题目分析:

我们可以发现代价其实就是我们封路的次数+剩下的最长路的长度。
假设原图是一个 DAG 的话,我们就可以考虑设 \(d[i]\) 表示 \(i\)\(n\) 的最小代价。
对于一个节点我们将它的所有后继的 \(d\) 进行降序排序之后,设为 \(s_1,s_2,s_3,\cdots\)因为我们封路显然要先封代价大的,所以就是:

\[d[i] = \min \{s_x + (x-1) + 1\}
\]

那么可是原图不是一个 DAG,关键有环怎么办呢?
我们可以设 \(c_i\) 代表所有没被更新的点的 \(d\) 值设为 \(+\infty\) 的前提下,\(i\) 点当前的 \(d\) 值。
显然发现我们可以每次选择所有 \(c\) 中最小的一个,作为对应点的 \(d\) 值,因为我们最小的 \(c\) 值不可能存在一种更新方式能让他更小,因为所有的方式都会加一个 \(x\),也就是封前多少条路,也就是更新出来的值一定大于等于原来的值。
我们这样弄其实就是相当于升序访问每一个节点的后继的 \(d\) 值,相应的弄弄就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int> 
using namespace std;
const int N = 4e5+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
int cnt,head[N],dis[N],out[N];
bool vis[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++){
		int u,v;
		scanf("%d%d",&u,&v);
		out[u]++;
		add_edge(v,u);
	}
	memset(dis,0x3f,sizeof(dis));
	priority_queue<PII> q;
	dis[n] = 0;
	q.push({0,n});
	while(!q.empty()){
		int now = q.top().second;q.pop();
		if(vis[now])	continue;
		vis[now] = true;
		for(int i = head[now]; i;i = e[i].nxt){
			int to = e[i].to;
			out[to]--;
			int x = dis[now] + out[to] + 1;
			//+ out[to] 是删去剩下的那些;+ 1 是这个路径长度
			if(dis[to] > x){
				dis[to] = x;
				q.push({-dis[to],to});
			} 
		}
	}
	printf("%d\n",dis[1]);
	return 0;
}

CF1691E Number of Groups

题目分析:

我们可以先对所有端点离散化一下。
可以发现如果对于某一个点,存在两种颜色线段都经过这个点,那么经过这个点的线段一定都可以相互连边,形成一个连通块。
那么就可以考虑将这种两种颜色都经过的点拿出来,算出只考虑这些点的时候区间的情况是什么样子的,然后最后求一个区间的并集,形成多少个区间答案就是多少。
因为如果两个区间在这种情况下有交意味着他们同时经过了某一个关键点,也就是他们一定可以相互连通。
为了方便区间求交,我们可以事先将区间弄成 \([l,r)\) 的形式,但是需要注意很多的细节。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N = 2e6+5;
struct node{
	int col,l,r;
}a[N];
int b[N],sum[2][N],tmp[N],tot,cnt;
bool cmp(node a,node b){
	if(a.l == b.l)	return a.r < b.r;
	return a.l < b.l;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%lld",&t);
	while(t--){
		tot = 0,cnt = 0;
		int n;
		scanf("%lld",&n);
		for(int i=1; i<=n; i++){
			scanf("%lld%lld%lld",&a[i].col,&a[i].l,&a[i].r);
			a[i].r++;//方便求并集 
			b[++tot] = a[i].l;b[++tot] = a[i].r;
		}
		//离散化 
		sort(b+1,b+tot+1);
		tot = unique(b+1,b+tot+1) - b - 1;
		for(int i=1; i<=n; i++){
			a[i].l = lower_bound(b+1,b+tot+1,a[i].l) - b;
			a[i].r = lower_bound(b+1,b+tot+1,a[i].r) - b;
		}
		//求每个点有什么颜色 
		for(int i=1; i<=n; i++){
			sum[a[i].col][a[i].l]++;
			sum[a[i].col][a[i].r]--;
		}
		for(int i=1; i<=tot; i++){
			sum[0][i] += sum[0][i-1];
			sum[1][i] += sum[1][i-1];
			if(sum[0][i] && sum[1][i]){   //得到关键点 
				tmp[++cnt] = i;
			}
		}
		//获取每个区间的关键点集合 
		for(int i=1; i<=n; i++){
			a[i].l = lower_bound(tmp+1,tmp+cnt+1,a[i].l) - tmp;
			a[i].r = lower_bound(tmp+1,tmp+cnt+1,a[i].r) - tmp - 1;
		}
		sort(a+1,a+n+1,cmp);
		int now_l = 0,now_r = 0; //求区间并集并统计答案 
		int ans = 0;
		for(int i=1; i<=n; i++){
			if(a[i].l > a[i].r)	ans++;
			else if(a[i].l > now_r){
				ans++;
				now_l = a[i].l,now_r = a[i].r;
			}
			else	now_r = max(now_r,a[i].r);
		}
		printf("%lld\n",ans);
		for(int i=0; i<=tot+2; i++)	for(int j=0; j<=1; j++)	sum[j][i] = 0;
	}
	return 0;
}

10.26

CF1681F Unique Occurrences

题目分析:

我们可以考虑对于每一种颜色单独考虑一下。
其实就是将这种颜色边去掉之后,形成许多的连通块,对于边 \((u,v)\) 的贡献就是其连接的两个连通块的大小的乘积。
这种连通性的问题,我们就可以考虑使用并查集来维护,外边再套一层线段树分治就可以了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int> 
using namespace std;
const int N = 5e5+5;
int ans = 0,fa[N],sz[N];
vector<PII> e[4 * N],c[N];
int find(int x){
	if(fa[x] ==  x)	return x;
	return find(fa[x]);
}
void modify(int now,int now_l,int now_r,int l,int r,PII w){
	if(l <= now_l && r >= now_r){
		e[now].push_back(w);
		return;
	}
	int mid = (now_l + now_r)>>1;
	if(l <= mid)	modify(now<<1,now_l,mid,l,r,w);
	if(r > mid)		modify(now<<1|1,mid+1,now_r,l,r,w);
}
void solve(int now,int now_l,int now_r){
	vector<int> tmp;
	for(auto i : e[now]){
		int u = find(i.first),v = find(i.second);
		if(sz[u] > sz[v])	swap(u,v);   //按秩合并 
		tmp.push_back(u);fa[u] = v;sz[v] += sz[u];
	}
	if(now_l == now_r){
		for(auto i : c[now_l]){
			ans += sz[find(i.first)] * sz[find(i.second)];
		}
	}
	else{
		int mid = (now_l + now_r)>>1;
		solve(now<<1,now_l,mid);solve(now<<1|1,mid+1,now_r);
	}
	reverse(tmp.begin(),tmp.end());  //撤销操作 
	for(int i : tmp){
		sz[fa[i]] -= sz[i];fa[i] = i; 
	}
}
signed main(){
	int n;
	scanf("%lld",&n);
	for(int i=1; i<n; i++){
		int u,v,col;
		scanf("%lld%lld%lld",&u,&v,&col);
		c[col].push_back({u,v});
		if(col > 1)	modify(1,1,n,1,col-1,{u,v});
		if(col < n)	modify(1,1,n,col+1,n,{u,v});
	}
	for(int i=1; i<=n; i++){
		fa[i] = i;
		sz[i] = 1;
	}
	solve(1,1,n);
	printf("%lld\n",ans);
	return 0;
}

CF1680D Dog Walking

题目分析:

我们对于原数组求一个前缀和之后,其实问题就是要让其中的最大值-最小值最大。
其实也就是让一段区间的和最大/最小。
观察这个数据范围,显然可以直接枚举这个区间是哪个。
如果没有和为 \(0\) 的限制的话,显然就是尽可能地加或者尽可能地减就好了。
但是有和为 \(0\) 的限制,其实也就是可以想象为:区间内部尽可能加,区间外部尽可能减,然后通过加减调整到相等,也就是两者的 \(\min\)
反之同理了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3005;
int a[N],sum[N],pre[N];
signed main(){
	int n,k;
	scanf("%lld%lld",&n,&k);
	for(int i=1; i<=n; i++){
		scanf("%lld",&a[i]);
		sum[i] = sum[i-1] + a[i];
		pre[i] = pre[i-1] + (a[i] == 0);
	}
	if(abs(sum[n]) > pre[n] * k){
		printf("-1\n");
		return 0;
	}
	int ans = -1;
	for(int l=1; l<=n; l++){
		for(int r=l; r<=n; r++){
			int sum1 = sum[r] - sum[l-1],cnt1 = pre[r] - pre[l-1];
			int sum2 = sum[n] - sum1,cnt2 = pre[n] - cnt1;
			ans = max(ans,min(abs(sum1 + cnt1 * k),abs(sum2 - cnt2 * k)));
			ans = max(ans,min(abs(sum1 - cnt1 * k),abs(sum2 + cnt2 * k)));
		}
	}
	printf("%lld\n",ans + 1);
	return 0;
}

CF1679E Typical Party in Dorm

题目分析:

我们可以考虑对原串进行预处理,预处理出 \(dp[S][j]\) 表示字符集为 \(S\),字符集大小为 \(j\) 的方案数。
那么为什么要来一个字符集大小为 \(j\) 的限制呢?因为我们对于字符集为 \(S\) 还需要考虑 \(T \subset S\) 的情况
显然可以考虑直接枚举中心点,然后去判断一下。
最后求一个高维前缀和就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int MOD = 998244353;
char s[N];
int all,dp[1<<18][18];
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%lld",&n);
	scanf("%s",s+1);
	for(int i=1; i<=n; i++)	all += s[i] == '?';
	for(int i=1; i<=n; i++){  //枚举中心点 
		int l = i + 1,r = i - 1;
		int cnt = 0,sta = 0;
		while(l > 1 && r < n && (s[l-1] == s[r+1] || s[l-1] == '?' || s[r+1] == '?')){
			l--,r++;
			if(s[l] == '?' && s[r] != '?') sta |= (1<<(s[r] - 'a'));
			if(s[l] != '?' && s[r] == '?') sta |= (1<<(s[l] - 'a'));
			if(l != r && (s[l] == '?' || s[r] == '?'))	cnt++;
			for(int j=1; j<=17; j++)	dp[sta][j] = mod(dp[sta][j] + power(j,all-cnt));
		}
		sta = cnt = 0;
		l = i + 1,r = i;
		while(l > 1 && r < n && (s[l-1] == s[r+1] || s[l-1] == '?' || s[r+1] == '?')){
			l--,r++;
			if(s[l] == '?' && s[r] != '?') sta |= (1<<(s[r] - 'a'));
			if(s[l] != '?' && s[r] == '?') sta |= (1<<(s[l] - 'a'));
			if(l != r && (s[l] == '?' || s[r] == '?'))	cnt++;
			for(int j=1; j<=17; j++)	dp[sta][j] = mod(dp[sta][j] + power(j,all-cnt));
		}
	}
	for(int i=1; i<=17; i++){
		for(int j=0; j<(1<<17); j++){
			if((j >> (i-1)) & 1){
				for(int k=1; k<=17; k++)	dp[j][k] = mod(dp[j][k] + dp[j ^ (1<<(i-1))][k]);
			}
		}
	}
	int q;
	scanf("%lld",&q);
	while(q--){
		scanf("%s",s+1);
		int sta = 0;
		int m = strlen(s+1);
		for(int i=1; i<=m; i++)	sta |= (1<<(s[i] - 'a'));
		printf("%lld\n",dp[sta][m]);
	}
	return 0;
}

CF1675G Sorting Pancakes

题目分析:

看到这个数据范围显然可以随便搞搞。
可以设 \(dp[i][j][k]\) 表示前 \(i\) 个数,总共放了 \(j\) 个球,最后一个放了 \(k\) 个的最小操作次数。
我们的操作其实可以理解为我们向右边拿球/给球,所以其实直接理解为与右边交换就好了。
但是这样可能会让右边成为负数啊。但这一定不会很优啊,也就是一定不会是最终的答案。
转移就是直接枚举小于 \(k\) 的数就好了。
这样的话复杂度是 \(O(n^4)\) 显然过不去。
但是我们发现对于一个 \(p\),他一定会被所有大于它的 \(k\) 更新,那么直接倒叙枚举 \(k\),记录一下之前的最小值然后更新一下 \(k\) 就好了,复杂度就降到了 \(o(n^3)\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 300;
const int INF = 1e9+5;
int dp[N][N][N],sum[N],a[N];
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	memset(dp,0x3f,sizeof(dp));
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++){
		scanf("%d",&a[i]);
		sum[i] = sum[i-1] + a[i];
	}
	dp[0][0][m] = 0;
	for(int i=0; i<n; i++){
		for(int j=0; j<=m; j++){
			int mn = INF;
			for(int k=m; k>=0; k--){
				mn = min(mn,dp[i][j][k]);
				if(j + k <= m)	dp[i+1][j + k][k] = min(dp[i+1][j+k][k],mn + abs(j + k - sum[i+1]));
			}
		}
	}
	int ans = INF;
	for(int i=0; i<=m; i++)	ans = min(ans,dp[n][m][i]);
	printf("%d\n",ans);
	return 0;
}

10.27

CF1749C Number Game

题目分析:

可以发现这个题目如果我们去判断答案是否可行是很简单的,而且答案也满足单调性,所以可以考虑二分答案。
判断的话就是根据贪心的策略,对于 \(Alice\) 每次选择合法的最大的值,\(Bob\) 每次选择最小的一个值。
然后模拟就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
const int INF = 1e9+5;
int n,a[N],b[N];
bool del[N];
bool check(int mid){
	for(int i=1; i<=n; i++)	del[i] = false;
	for(int i=1; i<=n; i++)	b[i] = a[i];
	b[n+1] = INF;
	for(int k=1; k<=mid; k++){
		int pos = 0;
		for(int i=1; i<=n; i++){
			if(b[i] <= mid - k + 1 && b[i] > b[pos] && !del[i]){
				pos = i;
			}
		}
		if(!pos)	return false;
		del[pos] = true;
		pos = n + 1;
		for(int i=1; i<=n; i++){
			if(b[i] < b[pos] && !del[i]){
				pos = i;
			}
		}
		b[pos] += mid - k + 1;
	}
	return true;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);
		for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
		int l = 1,r = n;
		int ans = 0;
		while(l <= r){
			int mid = (l + r)>>1;
			if(check(mid)){
				ans = mid;
				l = mid + 1;
			}
			else	r = mid - 1;
		}
		printf("%d\n",ans);
	}
	return 0;
}

CF1749D Counting Arrays

题目分析:

对于有两种以上的策略显然不好限制,但是考虑容斥:计算只有一种策略的方案数。
我们可以发现我们一直选择 \(1\),一定是一种策略,然后其他的策略均不可能也就是说对于位置 \(i\) 上的数一定被小于等于 \(i\) 的所有质数整除。
记他们的积为 \(x\),那么符合条件的数的个数就是 \(\lfloor \dfrac{m}{x} \rfloor\),然后去算一下就好了。
注意数组长度为 \([1,n]\) 内的所有数。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
const int N = 3e6+5;
int prime[N],tot;
bool flag[N];
int mod(int x){return ((x % MOD) + MOD)%MOD;}
void pre_work(int mx){
	for(int i=2; i<=mx; i++){
		if(!flag[i])	prime[++tot] = i;
		for(int j=1; j<=tot && i * prime[j] <= mx; j++){
			flag[i * prime[j]] = true;
			if(i % prime[j] == 0)	break;
		}
	}
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
	pre_work(300000);
	int n,m;
	scanf("%lld%lld",&n,&m);
	int ans = 0;
	int tmp = 1,res = 1;
	__int128 p = 1;
	for(int i=1; i<=n; i++){   //屮,a 的长度 [1,n],我以为就是 n,挂了好久屮!!!! 
		p = ((__int128) p * m)%MOD;
		ans = (ans + p)%MOD;
	}
	tmp = 1,res = 1;
	for(int i=1; i<=n; i++){
		if(!flag[i] && tmp <= m)	tmp *= i;
		if(tmp > m)	res = 0;
		else	res = mod(res * mod(m / tmp));
		ans = mod(ans - res);
	}
	printf("%lld\n",ans);
	return 0;
}

10.28

[NOIP2018 提高组] 旅行

题目分析:

考虑如果是一棵树的话怎么办:
\(1\) 开始 \(dfs\),每次都选择最小的一个儿子走过去。
但是给定的是一棵基环树,我们就考虑在基环树上删边,然后每一个在新生成的树上跑一边上述的过程就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int> 
using namespace std;
const int N = 1e5+5;
vector<int> v[N];
vector<int> tmp,ans;
vector<PII> c;
int del[4];
int pre[N];
bool flag,vis[N];
void solve(int now,int fath){
	tmp.push_back(now);
	for(int to : v[now]){
		if(to == fath)	continue;
		if((del[1] == now && del[2] == to) || (del[1] == to && del[2] == now))	continue;
		solve(to,now);
	}
}
void find_circle(int now,int fath){
	vis[now] = true;
	for(int i : v[now]){
		if(i == fath)	continue;
		pre[i] = now;
		if(vis[i]){
			c.push_back({i,now});
			while(now != i){
				c.push_back({now,pre[now]});
				now = pre[now];
			}
			flag = true;
			return;
		}
		find_circle(i,now);
		if(flag)	return;
	}
	if(flag)	return;
}
int main(){
//	freopen("travel.in","r",stdin);
//	freopen("travel.out","w",stdout);
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++){
		int from,to;
		scanf("%d%d",&from,&to);
		v[from].push_back(to);
		v[to].push_back(from);
	}
	for(int i=1; i<=n; i++)	sort(v[i].begin(),v[i].end());
	if(m == n - 1){
		solve(1,0);
		ans = tmp;
	}
	else{
		find_circle(1,0);
		for(auto i : c){
			del[1] = i.first,del[2] = i.second;
			tmp.clear();
			solve(1,0);
			if(tmp < ans || ans.empty())	ans = tmp;
		}
	}
	for(int i : ans)	printf("%d ",i);
	return 0;
}

[NOIP2018 提高组] 赛道修建

题目分析:

发现给定的是一棵树,那么肯定是跟树有什么关系。
看到最小值最大显然想到二分一个最小值是多少。
我们可以发现一条路径就是从某一个点出发的两条链拼接在一起,或者是从一个出发的一条链。
我们就从底向上 \(dfs\) 整棵树,将长度大于二分值的从这个点开始的链直接扔了,然后剩下的链进行拼接。
然后每个点只能向父亲传一条链,显然传我们没有拼接起来的最长的链,因为每条边只可以被经过一次,也就是我们只可以向上传递一条路径。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[2 * N];
int tot,head[N],cnt,limit;
void add_edge(int from,int to,int val){
	e[++tot] = edge(head[from],to,val);
	head[from] = tot;
}
int dfs(int now,int fath){
	multiset<int> st;
	for(int i = head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		int tmp = dfs(to,now) + e[i].val;
		if(tmp >= limit)	cnt++;
		else	st.insert(tmp);
	}
	int mx = 0;  //因为不能重复所以只可以向上传递一个,显然传递最大的是最优的
	while(!st.empty()){
		if(st.size() == 1){
			mx = max(mx,*st.begin());
			break;
		}
		int tmp = *st.begin();
		multiset<int>::iterator it = st.lower_bound(limit - tmp);   //找到最小的符合条件的值
		if(it == st.begin() && st.count(*it) == 1)	++it;
		if(it == st.end()){
			mx = max(mx,tmp);
			st.erase(st.begin());
		} 
		else{
			cnt++;
			st.erase(it);st.erase(st.begin());   //感觉这个先后顺序应该也很重要的 
		}
	} 
	return mx;
}
signed main(){
//	freopen("track.in","r",stdin);
//	freopen("track.out","w",stdout);
	int n,m;
	scanf("%lld%lld",&n,&m);
	for(int i=1; i<n; i++){
		int from,to,val;
		scanf("%lld%lld%lld",&from,&to,&val);
		add_edge(from,to,val);add_edge(to,from,val);
	}
	int l = 0,r = 1e9+5;
	int ans = 0;
	while(l <= r){
		int mid = (l + r)>>1;
		cnt=0;limit = mid;
		dfs(1,0);
		if(cnt >= m){
			ans = mid;
			l = mid + 1;
		}
		else	r = mid - 1;
	}
	printf("%lld\n",ans);
	return 0;
}

10.29

P5104 红包发红包

题目分析:

我们可以显然发现,我们每一次期望获得的钱数是 \(\dfrac{w}{2}\),那么第 \(k\) 个人期望抢到的钱数显然是 \(\dfrac{w}{2^k}\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 1e9+7;
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
	int w,n,k;
	scanf("%lld%lld%lld",&w,&n,&k);
	printf("%lld\n",mod(w * power(power(2,k),MOD-2)));
	return 0;
}

P6154 游走

题目分析:

注意本题是所有路径,一开始读错题了┭┮﹏┭┮
那么其实就是求出来所有的路径的总长度 \(sum\) 和路径条数 \(cnt\)
因为每条路径等概率选择,所以最后的答案就是 \(\dfrac{sum}{cnt}\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
const int N = 2e6+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
int cnt,head[N],deg[N],g[N],f[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
int mod(int x){
	return x % MOD;
} 
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
	int n,m;
	scanf("%lld%lld",&n,&m);
	for(int i=1; i<=m; i++){
		int from,to;
		scanf("%lld%lld",&from,&to);
		add_edge(to,from);
		deg[from]++;
	}
	queue<int> q;
	for(int i=1; i<=n; i++){
		g[i] = 1,f[i] = 0;
		if(deg[i] == 0){
			q.push(i);
		}
	}
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i;i = e[i].nxt){
			int to = e[i].to;
			f[to] = mod(f[to] + f[now] + g[now]);
			g[to] = mod(g[to] + g[now]);
			deg[to]--;
			if(deg[to] == 0)	q.push(to);
		}
	}
	int sum1 = 0,sum2 = 0;
	for(int i=1; i<=n; i++){
		sum1 = mod(sum1 + f[i]),sum2 = mod(sum2 + g[i]);
	}
	printf("%lld\n",mod(sum1 * power(sum2,MOD-2)));
	return 0;
}

[国家集训队]单选错位

题目分析:

我们如果可以求出每一个问题期望做对的次数,也就是做对的概率,然后对于所有的问题求一个和就好了。
我们发现对于问题 \(i\) 如果想要做对就是相当于 \(i-1\) 选择的答案等于 \(i\) 的正确答案,显然这种的情况数就是 \(\min(a[i-1],a[i])\),那么总方案数就是 \(a[i-1] \times a[i]\),那么对于问题 \(i\) 做对的概率就是 \(\frac{\min(a[i-1],a[i])}{a[i-1] \times a[i]} = \frac{1}{\max(a[i-1],a[i])}\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7+5;
int a[N];
signed main(){
	int n,A,B,C;
	scanf("%lld%lld%lld%lld%lld", &n, &A, &B, &C, a + 1);
	for (int i = 2; i <= n; i++)
		a[i] = ((long long) a[i - 1] * A + B) % 100000001;
	for (int i = 1; i <= n; i++)
		a[i] = a[i] % C + 1;
	double ans = 0;
	for(int i=2; i<=n; i++)	ans += 1.0 / (max(a[i],a[i-1]));
	ans += 1.0 / max(a[1],a[n]);
	printf("%.3f",ans);
	return 0;
}

[SPOJ] FAVDICE - Favorite Dice

题目分析:

显然可以考虑设 \(f[i]\) 表示抛到了 \(i\) 面的期望操作次数,那么转移就是:

\[f[i] = \frac{i}{n} f[i] + \frac{n-i}{n}f[i+1] + 1
\]

解一下就可以出来:

\[f[i] = f[i+1] + \frac{n}{n-i}
\]

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
double f[N];
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		int n;scanf("%d",&n);
		f[1] = 1;
		for(int i=1; i<n; i++)	f[i+1] = f[i] + 1.0 / ( 1.0 * (n-i) / n);
		printf("%.2f\n",f[n]);
	}
	return 0;
}

CF1042E Vasya and Magic Matrix

题目分析:

注意:题目要求求的是欧几里得距离的平方
因为我们可以任意的移动,所以就可以考虑按权值排序,这样对于每一个点,它可以移动到的点它的这个前缀。
不让设 \(f[i]\) 表示以第 \(i\) 个点为起点的期望,\(cnt\)\(a[j] < a[i]\)\(j\) 的个数。注意可能出现相等的情况,但是相等我们是跳不过去的。
那么转移就是:

\[\begin{aligned}
f[i]
&= \frac{1}{cnt} \sum_{a[j] < a[i]} (x_i - x_j)^2 + (y_i - y_j)^2 \\
&= \frac{1}{cnt} \sum_{a[j] < a[i]} x_i^2 - 2\times x_i\times x_j + x_j^2 + y_i^2 - 2\times y_i\times y_j + y_j^2
\end{aligned}
\]

然后对于 \(x\)\(y\) 的平方和和线性和同时维护一下就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int MOD = 998244353;
struct node{
	int x,y,val;
}a[N];
int f[N],cnt,A[N],B[N],C[N],D[N],E[N];
bool cmp(node a,node b){
	return a.val < b.val;
}
int mod(int x){
	return ((x % MOD)+MOD)%MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
	int n,m;
	scanf("%lld%lld",&n,&m);
	for(int i=1; i<=n; i++){
		for(int j=1; j<=m; j++){
			cnt++;
			a[cnt].x = i,a[cnt].y = j;
			scanf("%lld",&a[cnt].val);
		}
	}
	sort(a+1,a+cnt+1,cmp);
	int lst = 0;
	for(int i=1; i<=cnt; i++){
		if(a[i].val != a[i-1].val)	lst = i-1;
		int tmp = mod(lst * (a[i].x * a[i].x + a[i].y * a[i].y) + A[lst] + C[lst]);
		tmp = mod(tmp - 2 * a[i].x * B[lst] - 2 * a[i].y * D[lst]);
		f[i] = mod((tmp + E[lst]) * power(lst,MOD-2));
		A[i] = mod(A[i-1] + a[i].x * a[i].x);
		B[i] = mod(B[i-1] + a[i].x);
		C[i] = mod(C[i-1] + a[i].y * a[i].y);
		D[i] = mod(D[i-1] + a[i].y);
		E[i] = mod(E[i-1] + f[i]);
	}
	int x,y;
	scanf("%lld%lld",&x,&y);
	for(int i=1; i<=cnt; i++){
		if(a[i].x ==x && a[i].y == y){
			printf("%lld\n",f[i]);
			return 0;
		}
	}
	return 0;
}

P4316 绿豆蛙的归宿

题目分析:

显然可以考虑设 \(dp[i]\) 表示以 \(i\) 为起点的期望路径总长度,设 \(deg[i]\) 表示 \(i\) 的出边个数。
那么转移显然就是:

\[dp[i] = \frac{1}{deg[i]} \sum_{(i,j) \in E} (dis[from][to] + dp[j])
\]

用一下记忆化搜索就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[N];
bool vis[N];
double f[N];
int cnt,head[N],deg[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
}
double dp(int now){
	if(vis[now])	return f[now];
	double tmp = 0;vis[now] = true;
	for(int i = head[now]; i; i =e[i].nxt){
		int to = e[i].to;
		tmp += 1.0 * (e[i].val + dp(to)) / deg[now];
	}
	return f[now] = tmp;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++){
		int from,to,val;
		scanf("%d%d%d",&from,&to,&val);
		add_edge(from,to,val);
		deg[from]++;
	}
	printf("%.2f\n",dp(1));
	return 0;
}

[NOIP2016 提高组] 换教室

题目分析:

显然可以考虑设 \(dp[i][j][0/1]\) 表示前 \(i\) 堂课,已经申请了 \(j\) 次换教室,第 \(i\) 堂课不申请/申请换教室的期望体力总共的最小值。
那么转移就是一个分类讨论啦:

\[\begin{aligned}
dp[i][j][0] &= \min\{dp[i-1][j][0] + dis[c[i-1]][c[i]],dp[i-1][j][1] + k[i-1] \times dis[d[i-1]][c[i]] + \big(1 - k[i-1]\big) \times dis[c[i-1]][c[i]]\} \\
dp[i][j][1] &= \min\{ dp[i-1][j-1][0] + k[i] \times dis[c[i-1]][d[i]] + \big(1 - k[i]\big) \times dis[c[i-1]][c[i]],\\ &dp[i-1][j-1][1] + k[i] \times \big(k[i-1] \times dis[d[i-1]][d[i]] + \big(1 - k[i-1]\big) \times dis[c[i-1]][d[i]]\big) + \big(1 - k[i]\big) \times \big(k[i-1] \times dis[c[i]][d[i-1]] + \big(1 - k[i-1]\big) \times dis[c[i]][c[i-1]]\big) \}
\end{aligned}
\]

为了让消耗的体力尽可能小,显然每次应该走最短路,这数据范围跑一遍 \(Floyd\) 就好了,上面的 \(dis[i][j]\) 就代表 \(i,j\) 的最短路。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e3+5;
const double INF = 100000000000000000.0;
int dis[N][N];
double k[N],dp[N][N][2];
int c[N],d[N];
void chkmn(double &a,double b){
	if(a > b)	a = b;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	memset(dis,0x3f,sizeof(dis));
	int n,m,v,e;
	scanf("%d%d%d%d",&n,&m,&v,&e);
	for(int i=1; i<=n; i++)	scanf("%d",&c[i]);
	for(int i=1; i<=n; i++)	scanf("%d",&d[i]);
	for(int i=1; i<=n; i++)	scanf("%lf",&k[i]);
	for(int i=1; i<=e; i++){
		int from,to;
		int val;
		scanf("%d%d%d",&from,&to,&val);
		dis[from][to] = min(dis[from][to],val);
		dis[to][from] = min(dis[to][from],val); 
	}
	for(int k=1; k<=v; k++){
		for(int i=1; i<=v; i++){
			for(int j=1; j<=v; j++){
				dis[i][j] = min(dis[i][j],dis[i][k] + dis[k][j]);
			}
		}
		dis[k][k] = dis[k][0] = dis[0][k] = 0;
	}
	for(int i=0; i<=n; i++){
		for(int j=0; j<=m; j++){
			dp[i][j][0] = dp[i][j][1] = INF;
		}
	}
	dp[1][0][0] = dp[1][1][1] = 0;
	for(int i=2; i<=n; i++){
		for(int j=0; j<=min(i,m); j++){
			chkmn(dp[i][j][0],min(dp[i-1][j][0] + dis[c[i-1]][c[i]],dp[i-1][j][1] + k[i-1] * dis[d[i-1]][c[i]] + (1 - k[i-1]) * dis[c[i-1]][c[i]]));
			if(j != 0) chkmn(dp[i][j][1],min(dp[i-1][j-1][0] + k[i] * dis[c[i-1]][d[i]] + (1 - k[i]) * dis[c[i-1]][c[i]],dp[i-1][j-1][1] + k[i] * (k[i-1] * dis[d[i-1]][d[i]] + (1 - k[i-1]) * dis[c[i-1]][d[i]]) + (1 - k[i]) * (k[i-1] * dis[c[i]][d[i-1]] + (1 - k[i-1]) * dis[c[i]][c[i-1]])));
		}
	}
	double ans = INF;
	for(int i=0; i<=m; i++){
		chkmn(ans,min(dp[n][i][0],dp[n][i][1]));
	}
	printf("%.2f",ans);
	return 0;
}

P6046 纯粹容器

题目分析:

我们考虑对于每一种容器单独考虑。
\(f[i]\) 表示存活到 \(i\) 轮的期望,那么答案显然就是:

\[\sum_{i=1}^{n-1} f[i] \times i
\]

这个其实可能不是很好弄,就考虑转化一下,也就是下面这个式子:

\[\sum_{i=1}^{n-1} P(i \le x)
\]

其中 \(P(i \le x)\) 代表存活轮数大于等于 \(i\) 的概率,会发现和上面的式子是等价的。
考虑对于 \(a\) 这个容器会在什么情况下被打倒:设 \(pre[i]\) 表示 \(i\) 左边第一个大于它的容器的位置,\(suf[i]\) 表示右边第一个大于它的容器的位置。
若它被打倒,当且仅当 \([pre[i],i]\) 或者 \([i,suf[i]]\) 内的边都被删除了。这个时候其实我们并不关注究竟其他的边是什么样子的,因为只要这些边断掉它就一定被打倒,不断掉就一定不会被打倒。
根据容斥,我们设 \(P(A)\) 表示 \([pre[i],i]\) 全部被断掉的概率,\(P(B)\) 表示 \([i,suf[i]]\) 全部被断掉的概率,\(P(AB)\) 表示 \([pre[i],suf[i]]\) 全部被断掉的概率。
那么它被打倒的概率就是 \(P(A) + P(B) - P(AB)\),也就是 \(A\)\(B\) 断掉的概率,所以不被打倒的概率就是 \(1 - P(A) - P(B) + P(AB)\)
以下设 \(d_1\)\([pre[i],i]\) 的边数,\(d_2\)\([i,suf[i]]\) 的边数。
其实也就是:

\[\begin{aligned}
P(i \le x)
&= 1 - \dfrac{\binom{n - 1 - d_1}{i - d_1}}{\binom{n-1}{i}} - \dfrac{\binom{n - 1 - d_2}{i - d_2}}{\binom{n-1}{i}} + \dfrac{\binom{n - 1 - d_1 - d_2}{i - d_1 - d_2}}{\binom{n-1}{i}}
\end{aligned}
\]

其实也就是除了那些边随便选的方案数除以总方案数,也就是一定会给这些边留出位置。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 105;
const int MOD = 998244353;
int a[N],pre[N],suf[N],c[N][N];
int mod(int x){
	return ((x%MOD)+MOD)%MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
signed main(){
	int n;
	scanf("%lld",&n);
	for(int i=1; i<=n; i++)	scanf("%lld",&a[i]);
	for(int i=1; i<=n; i++){
		for(int j=i-1; j>=1; j--){
			if(a[j] > a[i]){
				pre[i] = i - j;
				break;
			}
		}
		for(int j=i+1; j<=n; j++){
			if(a[j] > a[i]){
				suf[i] = j - i;
				break;
			}
		}
	}
	c[0][0] = 1;
	for(int i=1; i<=n; i++){
		c[i][0] = 1;
		for(int j=1; j<=i; j++){
			c[i][j] = mod(c[i-1][j] + c[i-1][j-1]);
		}
	}
	for(int i=1; i<=n; i++){
		int ans = 0;
		for(int j=1; j<=n-1; j++){
			int tmp = 1;
			if(pre[i] && j - pre[i] >= 0)	tmp = mod(tmp - c[n - 1 - pre[i]][j - pre[i]] * power(c[n-1][j],MOD-2));
			if(suf[i] && j - suf[i] >= 0)	tmp = mod(tmp - c[n - 1 - suf[i]][j - suf[i]] * power(c[n-1][j],MOD-2));
			if(pre[i] && suf[i] && j - pre[i] - suf[i] >= 0)	tmp = mod(tmp + c[n - 1 - pre[i] - suf[i]][j - pre[i] - suf[i]] * power(c[n-1][j],MOD-2));
			ans = mod(ans + tmp);		
		}
		printf("%lld ",ans);
	}
	return 0;
}

10.30

[CSP-S 2022] 假期计划

题目分析:

显然我们可以考虑枚举 \(B,C\),然后就只需要对于每一个点预处理出离它和 \(1\) 距离合法的且权值最大的 \(3\) 个点,然后枚举判断一下就好了。
具体可以使用 \(n\)\(bfs\) 解决。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3005;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[N * 10];
bool tmp[N][N],vis[N];
int n,m,k,cnt,head[N],a[N][8],val[N];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void solve(int s,int *a){
	memset(vis,false,sizeof(vis));
	queue<pair<int,int> > q;
	q.push({s,0});
	while(!q.empty()){
		int now = q.front().first;int d = q.front().second;q.pop();
		if(d > k)	continue;
		if(vis[now])	continue;
		vis[now] = true;
		for(int i = head[now]; i;i=e[i].nxt){
			int to = e[i].to;
			if(vis[to])	continue;
			if(d + 1 <= k)	q.push({to,d + 1});
			tmp[s][to] = true;tmp[to][s] = true;
			if(tmp[to][1] || tmp[1][to]){
				for(int j=1; j<=3; j++){   //更新答案 
					if(val[to] > val[a[j]] && to != a[1] && to != a[2] && to != a[3]){
						a[j+3] = a[j+2];a[j+2] = a[j+1];a[j+1] = a[j];
						a[j] = to;
						break;
					}
				}
			}
		}
	}
}
bool chk(int a,int b,int c,int d){
	if(a == b || a == c || a == d)	return false;
	if(b == c || b == d)	return false;
	if(c == d)	return false;
	return true;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld%lld",&n,&m,&k);
	for(int i=2; i<=n; i++)	scanf("%lld",&val[i]);
	for(int i=1; i<=m; i++){
		int from,to;
		scanf("%lld%lld",&from,&to);
		add_edge(from,to);add_edge(to,from);
	}
	for(int i=1; i<=n; i++){  //对于每一个点,寻找最优的三个点,顺路求出所有符合条件的点 
		solve(i,a[i]);
	}
	int ans = 0;
	for(int i=2; i<=n; i++){   //枚举 B , C 
		for(int j=2; j<=n; j++){
			for(int l=1; l<=3; l++){
				if(!a[i][l])	continue;
				for(int r=1; r<=3; r++){
					if(!a[j][r])	continue;
					//这就相当于确定了四个值是什么了:A:a[l] B:i C:j D:a[r]
					int A = a[i][l],B = i,C = j,D = a[j][r]; 
					if(tmp[i][j] && chk(A,B,C,D)){
						ans = max(ans,val[A] + val[B] + val[C] + val[D]);
						break;
					}
				}
			}
		}
	}
	printf("%lld\n",ans);
	return 0;
}

[CSP-S 2022] 策略游戏

题目分析:

显然根据贪心地来想,小 \(Q\) 肯定会选择一个与 \(A_x\) 相乘最小的数,然后小 \(L\) 其实可以根据小 \(Q\) 的最优策略反推出哪个位置是最优的。
显然无论对于小 \(Q\) 还是小 \(L\) 他们所选择的有可能成为答案的值只可能是五个:最大的正数、最小的正数、最大的负数、最小的负数、\(0\)
线段树维护一下五个标记然后枚举一下就好了,这里不要自己去写贪心策略看看怎么是最优的,很容易挂。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N =1e5+5;
const int INF = 1e18+5;
struct node{
	int mx_z,mn_z,mx_f,mn_f;
	bool ze;
	node(){
		mx_z = mn_z = mx_f = mn_f = 0;
		ze = false;
	}
};
node merge(node a,node b){   //区间合并 
	node c;
	c.mn_f = min(a.mn_f,b.mn_f);
	c.mx_z = max(a.mx_z,b.mx_z);
	c.ze = a.ze | b.ze;
	if(a.mn_z == 0)	c.mn_z = b.mn_z;
	else if(b.mn_z == 0)	c.mn_z = a.mn_z;
	else	c.mn_z = min(a.mn_z,b.mn_z);
	if(a.mx_f == 0)	c.mx_f = b.mx_f;
	else if(b.mx_f == 0)	c.mx_f = a.mx_f;
	else	c.mx_f = max(a.mx_f,b.mx_f);
	return c;
}
struct Tree{
	node tr[4 * N];
	void pushup(int now){
		tr[now] = merge(tr[now<<1],tr[now<<1|1]);
	}
	void build(int now,int now_l,int now_r,int *a){
		if(now_l == now_r){
			if(a[now_l] > 0)	tr[now].mx_z = tr[now].mn_z = a[now_l];
			if(a[now_l] < 0)	tr[now].mx_f = tr[now].mn_f = a[now_l];
			if(a[now_l] == 0)	tr[now].ze = true;
			return;
		}
		int mid = (now_l + now_r)>>1;
		build(now<<1,now_l,mid,a);build(now<<1|1,mid+1,now_r,a);
		pushup(now);
	}
	node query(int now,int now_l,int now_r,int l,int r){
		if(l <= now_l && r >= now_r)	return tr[now];
		int mid = (now_l + now_r)>>1;
		if(r <= mid)	return query(now<<1,now_l,mid,l,r);
		else if(l >= mid+1)	return query(now<<1|1,mid+1,now_r,l,r);
		else	return merge(query(now<<1,now_l,mid,l,r),query(now<<1|1,mid+1,now_r,l,r));
	}
}A,B;
int n,m,q,a[N],b[N];
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld%lld",&n,&m,&q);
	for(int i=1; i<=n; i++)	scanf("%lld",&a[i]);
	A.build(1,1,n,a);
	for(int i=1; i<=m; i++)	scanf("%lld",&a[i]);
	B.build(1,1,m,a);
	for(int i=1; i<=q; i++){
		int l1,r1,l2,r2;
		scanf("%lld%lld%lld%lld",&l1,&r1,&l2,&r2);
		node l = A.query(1,1,n,l1,r1);
		node r = B.query(1,1,m,l2,r2);
		a[1] = l.mx_z;a[2] = l.mx_f;a[3] = l.mn_z;a[4] = l.mn_f;
		b[1] = r.mx_z;b[2] = r.mx_f;b[3] = r.mn_z;b[4] = r.mn_f;
		int ans = -INF;
		for(int j=1; j<=4; j++){
			if(a[j] == 0)	continue;
			int tmp = r.ze ? 0 : INF;
			for(int k=1; k<=4; k++){
				if(b[k] != 0){
					tmp = min(tmp,a[j] * b[k]);
				}
			}
			if(tmp != INF)	ans = max(ans,tmp);
		}
		if(ans < 0 && l.ze)	ans = 0;
		printf("%lld\n",ans);
	}
	return 0;
}

10.31

CF453A Little Pony and Expected Maximum

题目分析:

显然我们可以对于每一个值处理出来他为最大值的概率,然后根据期望的定义直接算就好了。
那么考虑最大值为 \(x\) 的概率其实容斥一下就是最大值小于等于 \(x\) 的概率减去最大值小于等于 \(x-1\) 的概率。
这个应该是很显然的。
那么对于最大值小于等于 \(x\) 的概率显然就是 \((\frac{x}{m})^n\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+5;
double f[N];
double power(double a,int b){
	double res = 1;
	while(b){
		if(b & 1)	res = res * a;
		a = a * a;
		b >>= 1;
	}
	return res;
}
int main(){
	int m,n;
	scanf("%d%d",&m,&n);
	//设 f[i] 表示最大值为 i 的概率
	for(int i=1; i<=m; i++){
		f[i] = power(1.0 * i/m,n) - power(1.0 * (i-1)/m,n);
	} 
	double ans = 0;
	for(int i=1; i<=m; i++)	ans = ans + f[i] * i;
	printf("%.7f",ans);
	return 0;
}

P3802 小魔女帕琪

题目分析:

我们可以计算出每个位置作为结束位置进行七重奏的期望次数,也就是概率然后求和就是答案。
可以显然地得出,对于 \(7\) 号位置它的概率就是(设 \(n = \sum_{i=1}^7 a_i\)):

\[\dfrac{a_1}{n} \times \dfrac{a_2}{n-1} \times \dfrac{a_3}{n-2} \times \dfrac{a_4}{n-3} \times \dfrac{a_5}{n-4} \times \dfrac{a_6}{n-5} \times \dfrac{a_7}{n-6}
\]

因为我们可以按照任意顺序取,所以最后应该是:

\[7! \times \dfrac{a_1}{n} \times \dfrac{a_2}{n-1} \times \dfrac{a_3}{n-2} \times \dfrac{a_4}{n-3} \times \dfrac{a_5}{n-4} \times \dfrac{a_6}{n-5} \times \dfrac{a_7}{n-6}
\]

虽然不同的顺序会导致每个分子对应的分母出现改变,但是其实乘积不会变也就不要紧了。
那么我们考虑扩展到第 \(8\) 个位置:
假设第 \(1\) 次取的是 \(i\) 那么触发七重奏的概率概率就是:

\[\dfrac{a_1}{n} \times \dfrac{a_2}{n-1} \times \dfrac{a_3}{n-2} \times \dfrac{a_4}{n-3} \times \dfrac{a_5}{n-4} \times \dfrac{a_6}{n-5} \times \dfrac{a_7}{n-6} \times \dfrac{a_i}{n-7}
\]

注意这个同上,是因为乘积不变而进行的变化的式子。
我们如果对于所有的 \(i\) 求一个和的话最后一项就是:\(\sum_{i=1}^7 \frac{a_i}{n-7} = 1\),那么也就是没有影响,也就是说一个关键的结论:
\(8\) 个位置触发七重奏的概率与 \(7\) 位置一样,同理可以扩展到任意的位置。
也就是对于任意位置触发七重奏的概率都与 \(7\) 位置一样,所以他们可以相同的去计算。
那么只要算出来在 \(7\) 位置的期望,那么乘以 \(n-6\) 就是答案。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int a[10];
int main(){
	int n = 0;
	for(int i=1; i<=7; i++)	scanf("%d",&a[i]),n+=a[i];
	if(n < 7){
		printf("0.000\n");
		return 0;
	}
	double ans = 1;
	for(int i=1; i<=7; i++)	ans = 1.0 * ans * a[i] / (n - i + 1) * i;
	ans = 1.0 * ans * (n - 6);
	printf("%.3f",ans);
	return 0;
}

CF16E Fish

题目分析:

看到这个数据范围显然想到状压。
可以设 \(dp[S]\) 表示鱼塘里剩余 \(S\) 这些鱼的概率。
然后每次就是枚举哪两条鱼遇见,乘上遇见的概率,然后再枚举谁获胜,乘上获胜的概率然后转移就好了。
注意不要转移重了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
double dp[1<<19];
double a[20][20];
int main(){
	int n;
	scanf("%d",&n);
	for(int i=0; i<n; i++){
		for(int j=0; j<n; j++){
			scanf("%lf",&a[i][j]);
		}
	}
	dp[(1<<n)-1] = 1;
	for(int i = (1<<n)-1; i>=0; i--){
		int cnt = __builtin_popcount(i);
		for(int l=0; l<n; l++){
			for(int r=l+1; r<n; r++){
				if(((i>>l)&1) && ((i>>r)&1) && l != r && cnt > 1){
					dp[i ^ (1<<l)] += 1.0 * dp[i] * a[r][l] / (cnt * (cnt - 1) / 2.0);
					dp[i ^ (1<<r)] += 1.0 * dp[i] * a[l][r] / (cnt * (cnt - 1) / 2.0);
				}
			}
		}
	}
	for(int i=0; i<n; i++){
		printf("%.6f ",dp[1<<i]);
	}
	return 0;
}

P4550 收集邮票

题目分析:

我们考虑如果我们进行了 \(x\) 步操作,那么也就意味着代价就是 \(\dfrac{x^2+x}{2}\),那么其实我们只需要维护一个 \(x\) 的期望,和一个 \(x^2\) 的期望就好了。
注意:\(E(x^2) \not= E(x)^2\),因为期望只是具有线性性,也就是 \(E(x+y) = E(x) + E(y)\)
那么就考虑怎么维护就好了:
\(f[i]\) 表示拿到 \(i\) 个邮票要全部买完,\(x\) 的和的期望,\(g[i]\) 表示拿到 \(i\) 个邮票要全部买完 \(x^2\) 的和的期望。

对于 \(f\) 显然:\(f[i] = \frac{i}{n}(f[i] + 1) + \frac{n-i}{n}(f[i+1]+1)\)
稍微解一下就可以得出:\(f[i] = f[i+1] + \frac{n}{n-1}\)

对于 \(g\) 显然:\(g[i] = \frac{i}{n}(g[i] + 2 \times f[i] + 1) + \frac{n-i}{n}(g[i+1] + 2\times f[i+1] + 1)\)
这一步是因为:\(E((x+1)^2) = E(x^2) + 2 \times E(x) + 1\)
依旧是稍微解一下就可以得出:\(g[i] = \frac{n}{n-i} + \frac{2\times i}{n-i} f_i + g_{i+1} + 2 \times f_{i+1}\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
double x[N],x2[N];
int main(){
	int n;
	scanf("%d",&n);
	x[n] = x2[n] = 0;
	for(int i=n-1; i>=0; i--){
		x[i] = x[i+1] + 1.0 * n / (n - i);
		x2[i] = 1.0 * i / (n-i) * (2 * x[i] + 1) + x2[i+1] + 2.0 * x[i+1] + 1;
	}
	printf("%.2f\n",(x[0] + x2[0]) / 2.0);
	return 0;
}

[HNOI2015]亚瑟王

题目分析:

根据期望的线性性我们显然可以对于每一张牌求一个使用概率,然后与伤害乘一下求一个和就是答案。
我们设 \(g[i]\) 表示第 \(i\) 张牌的实际使用概率,那么答案就是:\(\sum_{i=1}^n g[i] \times d[i]\)
那么对于使用概率,我们可以考虑使用 \(dp\),设 \(f[i][j]\) 表示在 \(r\) 轮中前 \(i\) 张牌使用了 \(j\) 张的概率。
那么可以显然发现:

\[g[i] = \sum_{i=0}^{\min(i-1,r)} f[i-1][j] \times (1 - (1 - p[i])^{r-j})
\]

其中 \((1 - p[i])\) 也就是它不被使用的概率,因为我们选择了一张牌就直接跳掉,所以实际考虑了第 \(i\) 张牌的次数就是 \(r-j\),所以全部不被选择概率就是 \((1 - p[i])^{r-j}\),所以用 \(1\) 减去就是被选择的概率。
那么发现了这个 \(dp\) 是可行的之后我们就考虑怎么求解 \(dp\) 值啦。
显然很好做了,就是判断第 \(i\) 张牌在整个 \(r\) 轮中是否被选择,然后用概率一乘就好了。

\[f[i][j] = f[i-1][j] \times (1 - p[i])^{r-j} + f[i-1][j-1] \times (1-(1 - p[i])^{r-j})
\]

至此,整个题就完美的解决了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
double f[250][250],p[N],d[N];
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		memset(f,0,sizeof(f));
		int n,r;
		scanf("%d%d",&n,&r);
		for(int i=1; i<=n; i++)	scanf("%lf%lf",&p[i],&d[i]);
		f[0][0] = 1;double ans = 0;
		for(int i=0; i<n; i++){
			for(int j=0; j<=min(i,r); j++){
				double tmp = pow(1-p[i+1],r-j);  //i+1 在剩下几轮都不会被选择的概率。 
				f[i+1][j] += f[i][j] * tmp;
				if(j < r)	f[i+1][j+1] += f[i][j] * (1 - tmp),ans += f[i][j] * (1 - tmp) * d[i+1];
			}
		}
		printf("%.10f\n",ans);
	}
	return 0;
}

[六省联考 2017] 分手是祝愿

题目分析:

可以发现一个显然的贪心:从后到前考虑每个位置,如果这个位置是亮的就关上。
可能不是很好想,但是正确性很显然。
我们考虑设 \(f[i]\) 表示将必须按的位置从 \(i\) 个变为 \(i-1\) 个的期望操作次数,那么可得:

\[\begin{aligned}
f[i]
&= \frac{i}{n} + \frac{n-i}{n} (1 + f[i+1] + f[i]) \\
&= \frac{n + (n-i)\times f[i+1]}{i}
\end{aligned}
\]

因为我们如果操作错了,意味着我们会多一个灯需要关闭,所以需要关回来。
这样就直接从需要操作的数量开始递推就好了,特判如果一开始就小于等于 \(k\) 那么直接输出就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int MOD = 100003;
int f[N],a[N],cnt = 0;
int mod(int x){
	return x % MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1) res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	} 
	return res;
}
signed main(){
	int n,k; 
	scanf("%lld%lld",&n,&k); 
	for(int i=1; i<=n; i++)	scanf("%lld",&a[i]); 
	for(int i=n; i>=1; i--){  //倒序操作,寻找必须操作的个数 
		if(a[i]){
			cnt++;
			for(int j=1; j*j<=i; j++){
				if(i % j == 0){
					a[j] ^= 1;
					if(j * j != i)	a[i / j] ^= 1;
				}
			}
		}
	}
	for(int i=n; i>=1; i--){   //为什么不从 cnt 开始?因为显然 f[cnt+1] != 0 
		int tmp = mod((n - i) * f[i+1]);
		f[i] = mod(mod(n + tmp) * power(i,MOD-2));
	}
	int ans = 0;
	if(cnt <= k)	ans = cnt;  //需要的数量 <=k 显然直接搞就好了 
	else{
		for(int i=cnt; i>k; i--)	ans = mod(ans + f[i]);  //不然一个个移动直到移动到 k 
		ans = mod(ans + k);
	}
	for(int i=1; i<=n; i++)	ans = mod(ans * i);
	printf("%lld\n",ans);
	return 0;
}

2022.10 小结

只能说这个月的很多题的题解都是 11 月补的(狂笑
也是这个月因为一些课程的原因自己没调整好自己,没有合理安排时间吧,所以其实我感觉我这个月挺颓的,至少没有抽出时间写题解。
尤其是这个月迎来了 CSP-S2 但是因为疫情 SD 不出意外的取消了,也让我因此心态好几天不稳定,但是 CSP-S2 VP 的成绩让我感觉还行,也让我稍微找到了一些动力。
因为距离 NOIP 也只剩一个月了,通过和学长的瞎扯交流也大致找到了自己这一个月应该干什么。
但是其实回忆一下这个月也干了不少事:字符串、数论、期望概率。
NOIP2022 加油!!!