ybt 神(bian)奇(tai)题目总结合集(下)

第五章 动态规划

树形DP

T24 权值统计

在这里插入图片描述
树形dp阴间起来基本分两种,一种是结合别的知识(比如中篇的强联通分量),一种是混乱且又臭又长的递推状态。此题两者兼有一点。
惯例设 f i f_i fi表示以 i i i为一端的 i i i子树内的结果,那么对于一个点 i i i,有关的答案只有两种,一种是以它为一端(即 f i f_i fi),一种是经过这点,后者具有一个性质:在这部分产生的价值为任意两个儿子的 f f f的积再乘上这点的权值 a i a_i ai.考虑怎么求这个价值,根据 ( a 1 + a 2 + a 3 + . . . + a k ) 2 = ∑ i = 1 k a i   2 + 2 × ∑ i = 1 n ∑ j = i + 1 n ( a i ∗ a j ) (a_1+a_2+a_3+...+a_k)^2=\sum\limits_{i=1}^{k}a_i\,^2+2\times\sum\limits_{i=1}^n\sum\limits_{j=i+1}^n (a_i*a_j) (a1+a2+a3+...+ak)2=i=1kai2+2×i=1nj=i+1n(aiaj),前面的平方的和与和的平方都不难求,所以可以用这个表示出这个价值,这道题就能够 O ( n ) O(n) O(n)求解了。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 1;
const int mod = 10086;
struct yjx{
	int nxt,to;
}e[N << 1];
long long ecnt = -1,head[N],up[N],sum[N],c[N],res;
void save(int x,int y){
	e[++ecnt].nxt = head[x];
	e[ecnt].to = y;
	head[x] = ecnt;
}
void dfs(int now,int fa){//稍微整理了一下格式
	int i,temp;
	up[now] = c[now];
	for(i = head[now];~i;i = e[i].nxt){
		temp = e[i].to;
		if(temp == fa) continue;
		dfs(temp,now);
		up[now] = (up[now] + (up[temp] * c[now] % mod)) % mod;
		sum[now] = (sum[now] + up[temp] * (up[now] - up[temp] * c[now] % mod + mod) % mod) % mod;
	}
	sum[now] = (sum[now] + c[now]) % mod;
	res = (res + sum[now]) % mod;
}
int main(){
	int i,n,x,y;
	scanf("%d",&n);
	memset(head,-1,sizeof(head));
	for(i = 1;i <= n;i++){
		scanf("%lld",&c[i]);
	}
	for(i = 1;i < n;i++){
		scanf("%d %d",&x,&y);
		save(x,y),save(y,x);
	}
	dfs(1,0);
	printf("%lld\n",res);
	return 0;
}

T25 树的合并

在这里插入图片描述求直径是一个老生常谈的话题了。
在中篇就涉及过一道有关直径的题,但是那道题是形成直径,并非在完整的树上寻找直径。总结一下寻找直径的方法:一种是树形DP直接求,另一种是两次dfs找到两个端点,前者适合求直径长度以及一些衍生的链长问题,后者适合求端点。其实都是 O ( n ) O(n) O(n)的,不过写起来复杂程度不同。此题显然用的是前一种。
直径就是树上最长链,为了能够达到 O ( n ) O(n) O(n)的效果,记 f i f_i fi g i g_i gi分别表示 i i i子树内和子树外的最长链长,那么 d = m a x ( f i , g i ) d= max(f_i,g_i) d=max(fi,gi). f i f_i fi并不难求,关键在于如何求 g i g_i gi.
g i g_i gi显然需要用它的父节点更新而来。能够更新 g i g_i gi的有两种,一种是 g f a g_{fa} gfa,一种是除 i i i这个子树的最长链。为了求后者,还需要维护一个子树内次长链 f 2 f2 f2。考虑到 f i f_i fi f 2 i f2_i f2i是倒序更新, g i g_i gi是正序更新且需要前两个数组,所以需要两次dfs,此题又需要求两个树的直径,所以需要四次dfs(令人窒息)。
求直径(以一棵为例)代码如下:

void dfs11(int now,int fa){
	int i,temp;
	for(i = head1[now];~i;i = e1[i].nxt){
		temp = e1[i].to;
		if(temp == fa) continue;
		dfs11(temp,now);
		if(f[temp] + 1 > f[now]) f2[now] = f[now],f[now] = f[temp] + 1,pos[now] = temp;
		else if(f[temp] + 1 > f2[now]) f2[now] = f[temp] + 1;	
	}
}
void dfs12(int now,int fa){
	int i,temp;
	if(pos[fa] == now) g[now] = max(g[fa],f2[fa]) + 1;
	else g[now] = max(g[fa],f[fa]) + 1;
	if(now == 1) g[now] = 0;
	len1[now] = max(f[now],g[now]);//求以该点为一端的最长链长,后续有用
	d1 = max(d1,len1[now]);//更新出这棵树的直径
	for(i = head1[now];~i;i = e1[i].nxt){
		temp = e1[i].to;
		if(temp == fa) continue;
		dfs12(temp,now);
	}	
}

两棵树合并后,新的直径显然只有两种可能,一种是原来两棵树较大的直径,另一种则是被连接的两端点的最长链长之和+1,即代码中的 l e n 1 [ i ] + l e n 2 [ j ] + 1. len1[i]+len2[j]+1. len1[i]+len2[j]+1.问题在于,如果直接暴力找,时间复杂度是 O ( n m ) O(nm) O(nm)的,之前费这么多劲搞的 O ( n ) O(n) O(n)求直径就白给了。
但是考虑到很多时候这个和并不会比直径更大,如果能够去掉这些情况,时间复杂度就得以优化。像单调队列一样思考,把两个 l e n len len数组进行排序,设两个指针,一个指1,一个指 n n n,枚举第一个指针 l l l,从大往小移动第二个指针 r r r,直到其和小于直径,那么 l e n 1 [ l ] len1[l] len1[l] l e n 2 [ r ] len2[r] len2[r]在此过程中产生的贡献就等于 ( ( n − r ) × ( l e n 1 [ l ] + 1 ) + r × d + ∑ i = r + 1 n l e n 2 [ r ] ) ((n-r)\times (len1[l]+1)+r\times d+\sum\limits_{i=r+1}^{n} len2[r]) ((nr)×(len1[l]+1)+r×d+i=r+1nlen2[r])。接着增加 l l l,那么 r r r再次获得左移的机会,以此类推,最终的时间复杂度是 O ( n + m ) O(n+m) O(n+m).如果追求线性(此题 O ( n l o g n ) O(nlogn) O(nlogn)级别可以通过),排序可以用桶排。
统计权值部分代码如下:

len2[0] = -2e9;
for(i = 1;i <= n;i++){
	while(len1[i] + len2[r] + 1 > d && r >= 0){
		pref += len2[r];
		--r;
	}
	res += (len1[i] + 1) * (m - r) + pref + d * r;
}

状压DP

T26 涂抹果酱

在这里插入图片描述此题首先要用一个性质 (否则就会写的和题解一样难看)
对于一个三进制的数 S S S,它第 i i i位(位数从右至左自0开始)的数字即为 S / 3 i . S / 3^i. S/3i.
利用这一性质,就能快速地取出这位的数字而不必搞大量数组,也就不会写的非常混乱。
首先处理一下一行所有可能的方案(顺便以此判断一下第 k k k行的方案是否是合法的),设 d p ( i , S ) dp(i,S) dp(i,S)表示第 i i i行的状态为 S S S的方案数,那么这可以从所有与上一行不重合的方案当中转移而来。利用上面的这个性质,可以非常方便地判断出来。
总之是一个多进制的经典题(但是在没有这个性质的时候做起来会非常痛苦),做法十分标准,练习很合适。所以此题算是一个比较难的模板。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mod = 1e6;
int n,m,k,cnt,st[251],mi[11];
long long res,dp[10001][251];
bool check(int x,int y){//判断两个状态是否重合
	int i;
	for(i = 0;i <= m - 1;i++){
		if((x / mi[i]) % 3 == (y / mi[i]) % 3) return 0; 
	}
	return 1;
}
int main(){
	int i,j,l,stat = 0,x;
	bool lipu;
	scanf("%d %d",&n,&m);
	scanf("%d",&k);
	mi[0] = 1;
	for(i = 1;i <= m;i++){
		scanf("%d",&x);
		stat = stat * 3 + x - 1;//求出第k行的状态
		mi[i] = mi[i - 1] * 3;
	}
	for(i = 0;i < mi[m];i++){
		lipu = 0;
		for(j = 0;j <= m - 2;j++){
			if((i / mi[j]) % 3 == (i / mi[j + 1]) % 3) lipu = 1;
			//处理出所有合法的方案
		}
		if(!lipu){
			st[++cnt] = i;
		}
	}
	lipu = 1;
	for(i = 1;i <= cnt;i++){
		if(stat == st[i]){
			lipu = 0;
			break;
		}
	}
	if(lipu){//第k行方案本身不合法
		printf("0\n");
		return 0;
	}
	if(k == 1) dp[1][stat] = 1;
	else{
		for(i = 1;i <= cnt;i++) dp[1][st[i]] = 1;
	}
	for(i = 2;i <= n;i++){
		if(i == k){//特殊处理
			for(j = 1;j <= cnt;j++){
				if(check(st[j],stat)){
					dp[i][stat] = (dp[i][stat] + dp[i - 1][st[j]]) % mod;
				}
			}
		}
		else if(i == k + 1){//另一个特殊处理
			for(j = 1;j <= cnt;j++){
				if(check(st[j],stat)){
					dp[i][st[j]] = (dp[i][st[j]] + dp[i - 1][stat]) % mod;
				}
			}
		}
		else{
			for(j = 1;j <= cnt;j++){
				for(l = 1;l <= cnt;l++){
					if(check(st[j],st[l])){
						dp[i][st[j]] = (dp[i][st[j]] + dp[i - 1][st[l]]) % mod;
					}
				}
			}
		}
	}
	for(i = 1;i <= cnt;i++){
		res = (res + dp[n][st[i]]) % mod;
	}
	printf("%lld\n",res);
	return 0;
}
/*
2 2
1
2 3
*/
T27 炮兵阵地

(洛谷P2704)
一道没啥技巧纯粹硬核的经典问题。

我在做这题的时候还处在逐渐熟悉位运算的阶段,这就是为什么一道经典题能够进入这个合集的原因…

首先炮的打击范围一侧长度为2,所以需要预处理一下前两行的状态,同时顺理成章的需要同时记录两行的状态,设 d p ( i , S , T ) dp(i,S,T) dp(i,S,T)表示处理到第 i i i行,这一行的状态是 S S S,前一行的状态是 T T T最多放的炮个数。这样一来,只需枚举这行及其前两行这三行的状态,就可以完成转移。
接下来设计状态。首先是这个地形,为了便于判断非法,地形状态方面设山坡为1,平原为0,阵型状态方面设放了炮为1,不放为0,如果在山坡上放了炮,就反映为与运算的结果不为0。炮与炮之间的关系如何处理就是一个比较经典的问题了,直接判断几个状态的与运算结果即可,别忘了由于更新顺序的关系,只需考虑向上和向左,另两个方向不必关心。
另外,考虑到此题 n ≤ 100 , m ≤ 10 n\leq 100,m\leq10 n100,m10,内存仅有125MB,开不下一个 n × 2 m + 1 n\times 2^{m+1} n×2m+1的数组,所以需要用滚动数组从0~2滚动,要注意如何处理滚动数组下标和取模(至少我经常因此翻车)。
代码如下:

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
long long dp[4][1 << 10 + 1][1 << 10 + 1],a[101],num[1 << 10 + 1];
int count(int s){//计算当前状态1的个数
	int ret = 0;
	while(s){
		if(s & 1) ++ret;
		s >>= 1;
	}
	return ret;
}
int main(){
	char s[11];
	int i,j,k,l,n,m;
	long long res = 0;
	scanf("%d %d",&n,&m);
	for(i = 0;i < n;i++){
		scanf("%s",s);
		for(j = 0;j < m;j++){
			a[i] = a[i] | ((s[j] == 'H') << j);
		}
	}
	for(i = 0;i < (1 << m);i++) num[i] = count(i);
	for(i = 0;i < (1 << m);i++){
		for(j = 0;j < (1 << m);j++){
			if((i & a[0] || (j & a[1]) || (i & j) || (i & (i << 1) || (i & (i << 2)) || (j & (j << 1))) || (j & (j << 2)))) continue;
			//非法状态,前三个是同一列2格内有炮,后四个是同一行2格内有炮
			dp[1][j][i] = num[i] + num[j];
		}
	}
	for(i = 2;i < n;i++){
		for(j = 0;j < (1 << m);j++){//本行状态
			if(j & a[i] || (j & (j << 1)) || (j & (j << 2))) continue;
			for(k = 0;k < (1 << m);k++){//前一行状态
				if(k & a[i - 1] || (j & k) || (k & (k << 1)) || (k & (k << 2))) continue;
				for(l = 0;l < (1 << m);l++){//再前一行状态
					if(l & a[i - 2] || (j & l) || (k & l) || (l & (l << 1)) || (l & (l << 2))) continue;
					dp[i % 3][j][k] = max(dp[i % 3][j][k],dp[(i - 1) % 3][k][l] + num[j]);
				}
			}
		}
	}
	for(i = 0;i < (1 << m);i++){
		for(j = 0;j < (1 << m);j++){
			res = max(res,dp[(n - 1) % 3][i][j]);
		}
	}
	printf("%lld\n",res);
	return 0;
}
T28 最短路径

在这里插入图片描述一道比较奇妙的状压DP。
首先可以求两个特殊点之间的最短路,但是经过标记点就不是最短路能够胜任的了,此题数据范围并不大(特殊点不多于10个,加上起点终点也不超过12个),所以可以直接从每一个特殊点跑Dijkstra,对于特殊点是否被走过可以进行状压转移。
最短路部分没什么可讲的,对于状态,设走过为1,未走为0,每一次更新一个状态,都用没有走过这点的状态更新,也就是对这一位取异或。这样转移就完成了。
此题是一个状压的灵活运用的开始,做的时候感觉并不简单,状压有点难写,比较需要思考(好在是否要用状压仅仅需要看数据范围就足以得出)。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<functional>
#include<utility>
using namespace std;
typedef pair<int,int> pr;
const int N = 5e4 + 1;
const int M = 1e5 + 1;
int ecnt = -1,head[N],pos[13],vis[N];
long long dis[N],f[13][1 << 12],dist[13][13];
struct yjx{
	int nxt,to,c;
}e[M];
void save(int x,int y,int w){
	e[++ecnt].nxt = head[x];
	e[ecnt].to = y;
	e[ecnt].c = w;
	head[x] = ecnt;
}
void Dijkstra(int st){//普通的最短路,记得每次清空
	int i,now,temp;
	priority_queue<pr,vector<pr>,greater<pr> > Q;
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[st] = 0;
	Q.push(make_pair(0,st));	
	while(!Q.empty()){
		now = Q.top().second;
		Q.pop();
		if(vis[now]) continue;
		vis[now] = 1;
		for(i = head[now];~i;i = e[i].nxt){
			temp = e[i].to;
			if(dis[temp] > dis[now] + e[i].c){
				dis[temp] = dis[now] + e[i].c;
				if(!vis[temp]) Q.push(make_pair(dis[temp],temp));
			}
		}
	}
}
int main(){
	int i,j,k,n,m,p,s,t,x,y,w;
	scanf("%d %d %d %d %d",&n,&m,&p,&s,&t);
	p += 2;
	memset(head,-1,sizeof(head));
	for(i = 1;i <= m;i++){
		scanf("%d %d %d",&x,&y,&w);
		save(x,y,w);
	}
	for(i = 2;i < p;i++){
		scanf("%d",&pos[i]);
	}
	pos[1] = s,pos[p] = t;
	for(i = 1;i <= p;i++){
		Dijkstra(pos[i]);
		for(j = 1;j <= p;j++){
			dist[i][j] = dis[pos[j]];
		}
	}
	if(dist[1][p] > 1e12){//不连通
		printf("-1\n");
		return 0;
	}
	memset(f,0x3f,sizeof(f));
	f[1][1] = 0;
	for(i = 1;i < (1 << p);i++){
		for(j = 1;j <= p;j++){//转移点
			if(!((i >> (j - 1)) & 1)) continue;//这一位为0,不能到达
			for(k = 1;k <= p;k++){//目标点
				if((i >> (k - 1)) & 1 || j == k) continue;//目标已经走到了
				f[k][i ^ (1 << (k - 1))] = min(f[k][i ^ (1 << (k - 1))],f[j][i] + dist[j][k]);
			}
		}
	}
	printf("%lld\n",f[p][(1 << p) - 1]);
	return 0;
}

T29 图的计数

在这里插入图片描述一道写起来相当相当烦人的状压。
由于此题仅仅关心奇偶性,所以也就可以只考虑状态的奇偶性。设 f ( i , j ) f(i,j) f(i,j)表示到第 i i i层奇偶状态为 j j j的方案数, g 1 ( i , j ) g1(i,j) g1(i,j)表示第 i i i行考虑到 j j j时的可达性状态, g 2 ( i , j ) g2(i,j) g2(i,j)则表示取反后的可达性状态,两者在更新的时候恰好逆过来。
具体操作直接写在注释里面了。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mod = 998244353;
int g1[10001][11],g2[10001][11];
long long f[10001][1 << 10];
int main(){
	int i,j,k,n,l,x,s1,s2,res = 0,tot;
	scanf("%d %d",&n,&k);
	for(i = 0;i < k;i++){
		scanf("%d",&x);
		g1[1][0] = g1[1][0] + (x << i);
	}
	for(i = 2;i < n - 1;i++){
		for(j = 0;j < k;j++){
			for(l = 0;l < k;l++){
				scanf("%d",&x);
				g1[i][j] = g1[i][j] + (x << l);
				g2[i][l] = g2[i][l] + (x << j);//预处理
			}
		}
	}
	for(i = 0;i < k;i++){
		scanf("%d",&x);
		g1[n][0] = g1[n][0] + (x << i);
	}
	f[2][g1[1][0]] = 1;
	for(i = 2;i < n;i++){
		for(j = 0;j < (1 << k);j++){
			if(f[i][j]){
				s1 = 0,s2 = 0;
				for(l = 0;l < k;l++){
					if(j & (1 << l)){
						s1 ^= g1[i][l],s2 ^= g2[i][l];//通过异或求得一部分点取反之后的状态
					}
				}
				f[i + 1][s1] = (f[i + 1][s1] + f[i][j]) % mod;//累计状态数
				f[i + 1][s2] = (f[i + 1][s2] + f[i][j]) % mod;
			}
		}
	}
	for(i = 0;i < (1 << k);i++){
		tot = 0;
		for(j = 0;j < k;j++){
			if((g1[n][0] & (1 << j)) && (i & (1 << j))) tot ^= 1;
			//求状态的奇偶性
		}
		if(!tot) res = (res + f[n - 1][i]) % mod;
	}
	printf("%d\n",res);
	return 0;
} 

单调队列

T30 粉刷木板

在这里插入图片描述显然,为了方便,要首先把所有粉刷匠按照 S i S_i Si排序。
d p ( i , j ) dp(i,j) dp(i,j)表示前 i i i个粉刷匠刷到第 j j j个木板的最大报酬,很明显,一个初始的转移为 d p ( i , j ) = m a x ( d p ( i − 1 , j ) , d p ( i , j − 1 ) ) dp(i,j)=max(dp(i-1,j),dp(i,j-1)) dp(i,j)=max(dp(i1,j),dp(i,j1)),除此之外,假设这个粉刷匠刷的区间为 [ k + 1 , j ] [k+1,j] [k+1,j],那么 d p ( i , j ) = m a x ( f ( i − 1 , k ) + p i × ( j − k ) )     ( j − L i ≤ k < S i ) dp(i,j)=max(f(i-1,k)+p_i\times(j-k))\,\,\,(j-L_i\leq k <S_i) dp(i,j)=max(f(i1,k)+pi×(jk))(jLik<Si)
化简得到 d p ( i , j ) = p i × j + m a x ( f ( i − 1 , k ) − p i × k )     ( j − L i ≤ k < S i ) dp(i,j)=p_i\times j+max(f(i-1,k)-p_i\times k)\,\,\,(j-L_i\leq k <S_i) dp(i,j)=pi×j+max(f(i1,k)pi×k)(jLik<Si),由于 i i i是枚举得到的,发现取max的部分仅仅与 k k k有关,而 k k k是单调的,所以能用单调队列维护。

此题是例题2,虽然确实是一个中规中矩的单调队列,不过算是相当难办,比后面几个例题都要难处理,不知为啥要选这个当例题2…

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M = 101;
const int N = 16001;
struct yjx{
	int l,p,s;
}a[M];
bool cmp(yjx x,yjx y){
	return x.s < y.s;
}
int f[M][N],Q[M];
int main(){
	int i,j,k,n,m,front = 1,rear = 0;
	scanf("%d %d",&n,&m);
	for(i = 1;i <= m;i++){
		scanf("%d %d %d",&a[i].l,&a[i].p,&a[i].s);
	}
	sort(a + 1,a + m + 1,cmp);
	for(i = 1;i <= m;i++){
		for(k = max(0,a[i].s - a[i].l);k < a[i].s;k++){
			while(front <= rear && f[i - 1][Q[rear]] - Q[rear] * a[i].p <= f[i - 1][k] - k * a[i].p){
				--rear;
			}
			Q[++rear] = k;//更新队尾
		}
		for(j = 1;j <= n;j++){
			f[i][j] = max(f[i - 1][j],f[i][j - 1]);//先取一个初始值
			if(j >= a[i].s){
				while(front <= rear && j - Q[front] > a[i].l) ++front;
				//维护合法区间
				if(front <= rear) f[i][j] = max(f[i][j],(j - Q[front]) * a[i].p + f[i - 1][Q[front]]);
			}
		}
	}
	printf("%d\n",f[m][n]);
	return 0;
}
T31 燃放烟火

(洛谷CF372C)

此题ybt的数据十分离谱,我样例都没过,交上去居然就AC了…

d p ( i , j ) dp(i,j) dp(i,j)表示放第 i i i个烟花,在 j j j位置获得的最大幸福度,那么显然有转移式:
d p ( i , j ) = m a x ( d p ( i − 1 , k ) + b i − ∣ a i − j ∣ )   ( j − d × Δ t ≤ k ≤ j + d × Δ t ) dp(i,j)=max(dp(i-1,k)+b_i-|a_i-j|)\,(j - d\times\Delta t \leq k\leq j+d\times \Delta t) dp(i,j)=max(dp(i1,k)+biaij)(jd×Δtkj+d×Δt)
k k k具有单调性,显然可以单调队列优化。为了便于维护两端的区间范围,可以正反各跑一次单调队列

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 150001;
const int M = 301;
int a[M],b[M],t[M],Q[N],f[2][N];
int main(){
	int i,j,k,n,m,d,temp,front,rear,res = -1e9;
	scanf("%d %d %d",&n,&m,&d);
	for(i = 1;i <= m;i++){
		scanf("%d %d %d",&a[i],&b[i],&t[i]);
		temp = d * (t[i] - t[i - 1]);
		front = 1,rear = 0;
		for(j = 1;j <= n;j++){
			while(front <= rear && Q[front] < j - temp) ++front;
			while(front <= rear && f[0][Q[rear]] < f[0][j]) --rear;
			Q[++rear] = j;
			f[1][j] = f[0][Q[front]] + b[i] - abs(a[i] - j); 
		}
		front = 1,rear = 0;
		for(j = n;j >= 1;j--){
			while(front <= rear && Q[front] > j + temp) ++front;
			while(front <= rear && f[0][Q[rear]] < f[0][j]) --rear;
			Q[++rear] = j;
			f[1][j] = max(f[1][j],f[0][Q[front]] + b[i] - abs(a[i] - j)); 
		}
		for(j = 1;j <= n;j++) f[0][j] = f[1][j];
	}
	for(i = 1;i <= n;i++){
		res = max(res,f[1][i]);
	}
	printf("%d\n",res);
	return 0;
}
T32 出题方案

在这里插入图片描述一道综合性比较强同时推导起来又非常恶心的单调队列 (难道纯粹的单调队列综合性还不够强?)
首先仍然考虑设计DP,从最简单的开始,设 d p i dp_i dpi表示到i为止的最小难度系数和,那么 d p i = d p j + a k ( j + 1 ≤ k ≤ i ) . dp_i=dp_j+a_k(j+1\leq k\leq i). dpi=dpj+ak(j+1ki).显然 a a a数组是可以放进单调队列进行优化的(可以去除在范围以外的元素)。
不要忘了单调队列里面存的是下标,假设取出一个元素 b k b_k bk,考虑到单调队列内的元素单调递减,那么在 b k b_k bk b k + 1 b_{k+1} bk+1以内的所有 d p dp dp转移时取的都应该为 a b k a_{b_k} abk,考虑到 d p dp dp应是单调不减的,为使 d p j + a b k dp_j+a_{b_k} dpj+abk最小, d p dp dp的下标应该为 b k − 1 b_{k-1} bk1.综上所述,我们维护出关于 a a a的(下标的)单调队列之后,需要再据此维护出一个 d p j + a j + 1 dp_j+a_{j+1} dpj+aj+1的集合,从中取最小值进行更新,这个动态取最小值的过程可以用multiset完成。
所以此题不仅仅是单调队列,还要利用其中的下标再维护一个multiset,相当于比一般的单调队列复杂了一倍,如果忘记单调队列实际上存的是下标而非数组,很容易卡在里面出不来(至少我是这样)。此题堪称一道难题。

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<cassert>
using namespace std;
const int N = 3e5 + 1;
multiset<int> S;
int n,m,Q[N],a[N],sum[N],f[N];
int main(){
	int i,j = 0,k,front = 1,rear = 0;
	scanf("%d %d",&n,&m);
	for(i = 1;i <= n;i++){
		scanf("%d",&a[i]);
		sum[i] = sum[i - 1] + a[i];
	}
	for(i = 0;i <= n;i++){
		while(sum[i] - sum[j] > m) ++j;//区间和过大,缩小
		while(front <= rear && Q[front] < j){
			if(rear - front > 0){
				S.erase(S.find(f[Q[front]] + a[Q[front + 1]]));
				//先维护multiset,再维护维护multiset的单调队列
			}
			++front;
		}
		while(front <= rear && a[Q[rear]] < a[i]){
			if(rear - front > 0){
				S.erase(S.find(f[Q[rear - 1]] + a[Q[rear]]));
			}
			--rear;
		}
		if(front <= rear) S.insert(f[Q[rear]] + a[i]);
		Q[++rear] = i;
		f[i] = f[j] + a[Q[front]];
		if(rear - front > 0) f[i] = min(*S.begin(),f[i]);
	}
	printf("%d\n",f[n]);
	return 0;
}

第六章 数学

质数与约数

T33 灯光控制

在这里插入图片描述
在做这道题之前,我们需要清楚一个性质:一个数 N N N至多有一个大于 N \sqrt{N} N 的质因子。
对于每一盏灯,控制它的开关即为它的质因子。根据上面提到的这个性质,对于每一盏灯,最多只会被大于 N \sqrt{N} N 的一个开关操作一次,因此假如我们处理出一种所有灯被小于 N \sqrt{N} N 的开关控制的情况,对于剩下的开关,只需暴力枚举打开之后灯的数目是否会变多,变多则打开。
考虑到 N ≤ 25 \sqrt{N}\leq25 N 25,所以实际上涉及到的质数只有9个,完全可以搜索出所有的开关情况然后处理剩下的开关,时间上足够了。
考虑到质数与因数这一章的整体代码量都不大,这个dfs+枚举已经算是实现难度高的了。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 1001;
int n,m,res,tot,cnt,prime[1001],ctrl[1001],light[1001];
bool isprime[1001],vis[1001];
void Euler(int n){
	int i,j;
	memset(isprime,1,sizeof(isprime));
	isprime[1] = 0;
	for(i = 2;i <= n;i++){
		if(isprime[i]) prime[++tot] = i;
		for(j = 1;j <= tot && i * prime[j] <= n;j++){
			isprime[i * prime[j]] = 1;
			if(i % prime[j] == 0) break;
		}
	}
}
void solve(){
	int i,j,sum = 0,temp;
	for(i = 1;i <= n;i++) sum += light[i];
	for(i = 1;i <= m;i++){
		if(ctrl[i] > (int)sqrt(n)){
			temp = 0;
			for(j = 1;ctrl[i] * j <= n;j++) temp += light[ctrl[i] * j];
			//printf("%d %d\n",temp,ctrl[i]);
			if(n / ctrl[i] - temp > temp) sum += n / ctrl[i] - (temp << 1);
		}
	}
	res = max(res,sum);
}
void dfs(int id){
	//printf("%d %d?\n",id,cnt);
	if(id > cnt){
		solve();
		return;
	}
	dfs(id + 1);
	int i;
	//printf("%d\n",prime[id]);
	if(vis[prime[id]]){
		for(i = 1;i * prime[id] <= n;i++) light[i * prime[id]] ^= 1;
		dfs(id + 1);
		for(i = 1;i * prime[id] <= n;i++) light[i * prime[id]] ^= 1; 
	}
}
int main(){
	int i,t,x;
	scanf("%d",&t);
	while(t--){
		tot = 0,cnt = 0;
		memset(light,0,sizeof(light));
		memset(vis,0,sizeof(vis));
		scanf("%d %d",&n,&m);
		Euler(n);
		for(i = 2;i <= (int)sqrt(n);i++){
			if(isprime[i]) ++cnt;
		}
		for(i = 1;i <= m;i++){
			scanf("%d",&ctrl[i]);
			vis[ctrl[i]] = 1;
		}
		res = 0;
		dfs(1);
		printf("%d\n",res);
	}
}

博弈论

T34 剪纸游戏

在这里插入图片描述一个标准的SG函数问题。
首先要思考必败态,由于SG函数要求一个必败态,而只知道必胜态,所以需要琢磨一下把哪些设为必败态,根据举例,发现可以把 ( 2 , 2 )    ( 2 , 3 )    ( 3 , 2 ) (2,2)\,\,(2,3)\,\,(3,2) (2,2)(2,3)(3,2)作为必败态。
剩下的就非常套路了,对于一张 n × m n\times m n×m的纸,其后续状态只有 n × ( m − i ) n\times(m-i) n×(mi)以及 ( n − j ) × m (n-j)\times m (nj)×m两种,对这两种状态SG值异或,就能求出当前状态的SG值。对所有的SG值作mex运算,就能求出这张 n × m n\times m n×m的纸的SG函数值。
总之SG函数对于我是一个自带高难度的东西,因此这题作为一个模板而上榜。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int a[101],sg[1001][1001];
int SG(int n,int m){
	if(sg[n][m] != -1) return sg[n][m];
	int i,k = 0,ret = 0;
	bool flag[2001];
	memset(flag,0,sizeof(flag));
	for(i = 2;2 * i <= n;i++){
		flag[SG(i,m) ^ SG(n - i,m)] = 1;
		//求两个后续状态对应的当前状态的SG值
	}
	for(i = 2;2 * i <= m;i++){
		flag[SG(n,i) ^ SG(n,m - i)] = 1;
	}
	while(flag[ret]) ++ret;//mex运算
	return sg[n][m] = sg[m][n] = ret;
	
}
int main(){
	int i,n,m,res;
	memset(sg,-1,sizeof(sg));
	sg[2][2] = sg[2][3] = sg[3][2] = 0;
	while(scanf("%d %d",&n,&m) != EOF){
		res = SG(n,m);
		if(res) printf("WIN\n");
		else printf("LOSE\n");
	}
	return 0;
}

T35 砖块游戏

在这里插入图片描述
一道名副其实的、令人喷饭的阴间题。
首先这题的感觉非常像一个SG函数问题,考虑到这题的砖块只有1-4四种,不用SG函数这种阴间玩意儿,设 d p ( a , b , c , d ) dp(a,b,c,d) dp(a,b,c,d)表示长度为1-4的砖块分别有a-d种时先手是否必胜,显然,只要能够到达这一状态的存在任何一种必败态,该状态就一定是一个必胜态,那么大致的转移可以这样表示:
d p n o w = ( ! d p 1 ) ∣ ( ! d p 2 ) ∣ ( ! d p 3 ) ∣ . . . ∣ ( ! d p k ) dp_{now}=(!dp_1)|(!dp_2)|(!dp_3)|...|(!dp_k) dpnow=(!dp1)(!dp2)(!dp3)...(!dpk)
至于具体的转移,对于取走,有以下四种转移:
d p ( a − 1 , b , c , d )     ( a ≥ 1 ) dp(a-1,b,c,d)\,\,\,(a\geq1) dp(a1,b,c,d)(a1)
d p ( a , b − 2 , c , d )     ( a ≥ 2 ) dp(a,b-2,c,d)\,\,\,(a\geq2) dp(a,b2,c,d)(a2)
d p ( a , b , c − 3 , d )     ( a ≥ 3 ) dp(a,b,c-3,d)\,\,\,(a\geq3) dp(a,b,c3,d)(a3)
d p ( a , b , c , d − 4 )     ( a ≥ 4 ) dp(a,b,c,d-4)\,\,\,(a\geq4) dp(a,b,c,d4)(a4)
对于分解,有以下四种转移:
d p ( a + 2 , b − 1 , c , d )     ( b ≥ 1 ) dp(a+2,b-1,c,d)\,\,\,(b\geq1) dp(a+2,b1,c,d)(b1)
d p ( a + 1 , b + 1 , c − 1 , d )     ( c ≥ 1 ) dp(a+1,b+1,c-1,d)\,\,\,(c\geq1) dp(a+1,b+1,c1,d)(c1)
d p ( a + 1 , b , c + 1 , d − 1 )     ( d ≥ 1 ) dp(a+1,b,c+1,d-1)\,\,\,(d\geq1) dp(a+1,b,c+1,d1)(d1)
d p ( a , b + 2 , c , d )     ( d ≥ 1 ) dp(a,b+2,c,d)\,\,\,(d\geq1) dp(a,b+2,c,d)(d1)
这样一来直接暴力枚举 a b c d abcd abcd四维转移即可,显然时间复杂度是 O ( a b c d ) O(abcd) O(abcd),可以得70分。
离谱的是,这题的100分数据范围非常大( 1 0 10000 10^{10000} 1010000),显然我们需要一种基本是 O ( 1 ) O(1) O(1)的算法。
考虑一下上面这几个转移式,经过一番离谱的观察 (想了很久都想不出来,无奈去看题解) ,设 r = ( a + c )   m o d   2 , s = ( b + d )   m o d   3 r=(a+c)\,mod\,2,s=(b+d)\,mod\,3 r=(a+c)mod2,s=(b+d)mod3,会发现如果 r ≠ s r\neq s r=s,其差值只有-1,1,2三种,而对于每一种差值,总是存在两种方式使得 r = s r=s r=s,且互相需要的状态不重合。也就是每一次 r ≠ s r\neq s r=s,总有一种方式变为 r = s r=s r=s,因此 r ≠ s r\neq s r=s是必胜的, r = s r=s r=s是必败的。
为了取模,需要一边读入一边取模,时间上足以通过。
总之这个优化非常离谱,不看题解真的想不到设这么两个玩意儿。只能说掌握70分怎么写,以及怎么推导必胜必败态就够了。不要忘了博弈论毕竟国赛级别的东西…

读入取模代码如下:

int read(int mod) {
    char w;
    while (w = getchar(), w < '0' || w > '9') {
    }
    int res = 0;
    while (w >= '0' && w <= '9') {
        res = (res * 10 + w - '0') % mod;
        w = getchar();
    }
    return res;
}
T36 黑白棋

在这里插入图片描述在这里插入图片描述此题乍一看没什么思路,不妨结合样例思考一下:

在这里插入图片描述
我们可以发现几个规律:(以下全部假设白棋先动)如果这个棋子的分布形如"WWW.B"或者"WWWBB",白棋连续的棋子一定会被连续吃掉。为了避免这种情况,白棋一定要先移动剩下全部可以移动的棋子把先手推给黑棋,黑棋也同理,直到双方形成这种直接能决出胜负的局面。因此我们确定,在研究情况的时候,研究的是直接对峙的两个黑白棋(即最右侧的白棋和最左侧的黑棋)。
问题在于,以上两个例子具有特殊性,无法概括全部的情况。具体地说,这两个例子中前者需要仅有一个空格,后者需要黑棋一方有至少两个棋子,那么我们需要考虑没有这两个特殊情况的其他情况。
如果两个棋子的间距超过了1格,那么双方可以逐步向中间移动,如果移动了之后棋子的间距就是1格,那就变为第一种情况;否则,先手向前移动一步,仍然是1格间距,但是先后手变换。
如果出现了“WWWB.”,或者其实“WB.”也足够,那么白棋直接吃掉黑棋,变成第三种情况。
另外,如果出现一种".WBB",即不构成连吃威胁的局面,白棋必定是吃掉黑棋,如果黑棋吃掉白棋,仍然是第三种情况;否则转化成第四种,被白棋吃掉,再变回第三种。
最后总结一下,所有的情况最终都归为第三种,而第三种又都归为第一种或者第二种,我们只需要分类讨论后几个情况,然后进入第三种判断哪一方会被连续吃子输掉游戏即可。
总之此题的完整情况相当不好想,需要有非常清晰的思路,能够把个别归到整体,还得从样例中提取出关键信息,还得有比较清晰的实现思路。非常有意思的一道题。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 1;
int n;
char s[N];
bool Duel(bool ed){//W...B,ed=1表示白棋已经行动过
	int i,l = 0,r = 0,len,temp;
	long long step; 
	for(i = n;i >= 1;i--){
		if(s[i] == 'W'){
			l = i;
			break;
		}
	}
	if(!l) return 0;
	for(i = 1;i <= n;i++){
		if(s[i] == 'B'){
			r = i;
			break;
		}
	}//找到中间的两个棋子
	if(!r) return 1;//黑棋已经被吃光了
	step = ed;
	//如果白棋已动,就产生了一个空位;也可以理解为黑棋要额外动一步,损失一步的机会
	if(r - l > 2){
		len = (r - l - 2) >> 1;
		l += len,r -= len;		
	}
	if(r - l > 2){
		if(ed) r--;//先手前进一步
		else l++;
	}
	for(i = l,temp = l;i >= 1;i--){
		if(s[i] == 'W'){
			step += temp - i;//计算白棋剩余可走步数
			--temp;
		}
	}
	for(i = r,temp = r;i <= n;i++){
		if(s[i] == 'B'){
			step -= i - temp;//计算白棋是否比黑棋的可走步数更多
			++temp;
		}
	}
	return step > 0;
}
bool Case1(int l,int r){//WB.
	s[r] = 'W';
	s[l] = '.';//吃棋
	return Duel(1);
}
bool Case2(int l,int r){//WBB
	int ret = 0;
	s[l] = '.';
	ret |= Duel(0);
	s[l + 2] = '.';
	ret |= Duel(0);
	return ret;
}
bool solve(int l,int r){//分类讨论
	if(r - l == 1){
		if(r == n || s[r + 1] == '.') return Case1(l,r);
		else if(l > 1 && s[l - 1] == 'W') return Duel(0);//WWBB
		else return Case2(l,r);
	}
	else return Duel(0);
}
int main(){
	int i,t,l,r;
	scanf("%d",&t);
	while(t--){
		l = 0,r = 0;
		scanf("%s",s + 1);
		n = strlen(s + 1);	
		for(i = n;i >= 1;i--){
			if(s[i] == 'W'){
				l = i;
				break;
			}
		}
		for(i = 1;i <= n;i++){
			if(s[i] == 'B'){
				r = i;
				break;
			}
		}
		if(solve(l,r)) printf("W\n");
		else printf("B\n");	
	}
	return 0;
}

期望问题

T37 电影问题

在这里插入图片描述

期望是概率问题当中我认为目前接触到的最麻烦的一项,就在于它的计算方法往往比较超乎想象,初等表达式很容易推导出问题或者干脆推导不出来,结合dp之后难度反而会变低一点。期望也是我差的题目最多的一章(2道),实在做不出来了…

此题先简单概括一下:假如小明喜欢一部电影,就会看了这部电影;如果不喜欢这部电影,就会把以前喜欢看的看一遍。
首先这个看电影的顺序是我们自己定的,我们要找一个最好的方案。假设有两部电影 i i i j j j,先看 i i i后看 j j j,这时 i i i的期望价值就是 l i × ( x i y i + y j − x j y j ) l_i\times (\frac{x_i}{y_i}+\frac{y_j-x_j}{y_j}) li×(yixi+yjyjxj) j j j同理。将两者进行比较,经过化简后发现, l i × x i y i × y j − x j y j > l j × x j y j × y i − x i y i l_i\times \frac{x_i}{y_i} \times \frac{y_j-x_j}{y_j}>l_j\times \frac{x_j}{y_j} \times \frac{y_i-x_i}{y_i} li×yixi×yjyjxj>lj×yjxj×yiyixi时,先看 i i i更好。由此,我们就能排出一个最佳的看电影顺序。
在解决排序的问题之后,我们需要计算一下每部电影的期望。喜欢一部电影会产生 l i × x i y i l_i\times\frac{x_i}{y_i} li×yixi的期望,而不喜欢一部电影先加上 − l i × y i − x i y i -l_i\times\frac{y_i-x_i}{y_i} li×yiyixi的期望,而后会加上看之前喜欢的全部电影的期望,因此还需要一个 s u m sum sum记录一下之前喜欢每一部电影的期望。把这些加在一起就是一部电影产生的期望,全部电影的总期望也就可以计算了。
此题看题面逻辑非常简单明了,然而在期望的计算和决策上其实并不简单,翻车率很高。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 2e6 + 2;
const int mod = 1004535809;
struct yjx{
	long long l,x,y; 
}a[N];
long long inv[N >> 1];
bool cmp(yjx p,yjx q){
	return p.l * p.x * (q.y - q.x) > q.l * q.x * (p.y - p.x);
}
int main(){
	int i,n;
	long long res = 0,sum = 0;
	scanf("%d",&n);
	for(i = 1;i <= n;i++){
		scanf("%lld %lld %lld",&a[i].l,&a[i].x,&a[i].y);
	}
	inv[1] = 1;
    for (i = 2; i <= N >> 1; i++) {
        inv[i] = inv[mod % i] * (mod - mod / i) % mod;//线性求逆元
    }
	sort(a + 1,a + n + 1,cmp);
	for(i = 1;i <= n;i++){
		res = (res + ((a[i].l * (a[i].x * 2 - a[i].y + mod) % mod + (a[i].y - a[i].x) * sum % mod)) * inv[a[i].y]) % mod;
		//这是经过化简之后的式子
		sum = (sum + a[i].l * a[i].x % mod * inv[a[i].y] % mod) % mod;//累计sum
	}
	printf("%lld\n",res);
	return 0;
}
T38 彩球抽取

在这里插入图片描述
这题乍一看根本就无从下手,好像根本分析不出什么球的期望。
这道题其实是一道概率DP,我们从DP的角度分析就会找到一点思路。
d p ( i , j , k ) dp(i,j,k) dp(i,j,k)表示取到第 i i i轮,第 j j j个球有 k k k个的概率,那么答案统计的也就是 i × d p ( i , j , k ) i\times dp(i,j,k) i×dp(i,j,k)的总和。
考虑怎么转移这个dp,如果现在就考虑 j j j这个球,每次取出两个球,无非是四种情况:第一个是 j j j,第二个是 j j j,全是 j j j,无 j . j. j.后两个其实可以算一种,因为都不改变 j j j的数量,第一种会使 j j j多一个,第二种会使 j j j少一个,此时概率DP的作用就发挥出来了,我们不必考虑具体这两个球怎么取,只需要直接从上一轮取球转移,如下:

for (j = 1; j <= 26; j++) {
    for (k = 1; k < n; k++) {
        temp = 1.0 * k * (n - k) / (1.0 * n * (n - 1));
        /*temp表示n个球中有k个球j(当然实际总数并非k,只是从此转移),
        取出一个球j的概率,有点组合数学的感觉*/
        dp[1][j][k + 1] += dp[0][j][k] * temp;//j增加一个的概率
        dp[1][j][k - 1] += dp[0][j][k] * temp;//j减少一个的概率
        dp[1][j][k] += dp[0][j][k] * (1 - 2 * temp);//除去上两种情况的概率
    }
}

通过这一种方式,就完成了一轮的转移。问题在于这个转移到底进行多少轮,一个奇妙的事情在于,dp一直在乘一个小数,随着轮数的增加这个dp的值会越来越小,直至在六位小数下能够被忽略。因此我们只需要看着时间限制尽可能多取几轮,轮数也不必记录,直接上滚动数组即可。
最终的代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int cnt[27];
double dp[2][27][27];
int main(){
	int i,j,k,n;
	char s[27];
	double res = 0,temp;
	scanf("%s",s + 1);
	n = strlen(s + 1);
	for(i = 1;i <= n;i++) ++cnt[s[i] - 'A' + 1];
	for(i = 1;i <= 26;i++) dp[1][i][cnt[i]] = 1;
	for(i = 0;i <= 100000;i++){
		for(j = 1;j <= 26;j++){
			res += i * dp[1][j][n];//累计答案
		}
		for(j = 1;j <= 26;j++){
			for(k = 1;k <= n;k++){//转移滚动数组
				dp[0][j][k] = dp[1][j][k];
				dp[1][j][k] = 0;
			}
		}
		for(j = 1;j <= 26;j++){
			for(k = 1;k < n;k++){
				temp = 1.0 * k * (n - k) / (1.0 * n * (n - 1));
				dp[1][j][k + 1] += dp[0][j][k] * temp;
				dp[1][j][k - 1] += dp[0][j][k] * temp;
				dp[1][j][k] += dp[0][j][k] * (1 - 2 * temp);
			}
		}
	}
	printf("%.6lf\n",res);
	return 0;
}

完结撒花!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值