dp一遍通+ybt题解

前言

马上csp-s考试了,却发现自己dp太菜了,打算恶补dp

线性dp理解

递推/记忆化搜索,有很多种理解方式
递归重叠子问题的记忆化搜索

像这里例如 \(f[3]\) 可以通过一次计算得到,保存答案,下一次直接调用即可,省去很多复杂度

我们从此引出dp第一个性质:最优子结构
大问题的最优解包含小问题的最优解,并且小问题的最优解可以推导出大问题的最优解

递推
我们不管dp[i-1]是多少,但可以从dp[i-1]推导得出dp[i]

dp第二个性质:无后效性
未来与过去无关

dp实现方法

自顶而下:大问题拆解成小问题求解
常用递归+记忆化实现
自底而上:小问题组合成大问题求解
常用制表递推实现
这是最常见的dp方法

背包dp

0/1背包

状态设计

\(dp[i][j]\) 表示只装前i个物品,体积为j时的最大价值

转移方程

\(c[i],v[i]\)表示第i个物品的体积和价值

方式1:

\[dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]) \]

方式2:

\[dp[i+1][j+w[i+1]]=max(dp[i+1][j+w[i+1]],dp[i][j]+v[i+1]) \]

区别:

方式一是通过上一维来推导这一维,方式二是通过这一维推导下一维

滚动数组

因为看到 \(dp[i][]\) 这一维只与 \(dp[i-1][]\) 这一维有关,所以可以压掉一维,优化空间

交替滚动

\(dp[1][]\)\(dp[0][]\) 交替滚动,逻辑清晰,建议初学者食用

int dp[2][N];
int now=0,old=1;
for(int i=1;i<=n;i++){
	swap(now,old);
	for(int j=0;j<=C;j++){
		if(j>=w[i])  dp[now][j]=max(dp[old][j],dp[old][j-w[i]]+c[i]);
		else  dp[now][j]=dp[old][j];
	}
}

自我滚动

int dp[N];
for(int i=1;i<=n;i++){
	for(int j=C;j>=w[i];j--){
		dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
	}
}

注:j应该反过来循环,因为 要保证 \(dp[j-w[i]]\) 这一位还是商议维的答案,没有被覆盖掉
规律:只要这一位(i这一位)修改的地方已经用过了,不会再用了,就对答案无影响

分组背包

把物品分为n组,每组只能选一个
只要每组枚举选哪个,0/1背包哪组选或不选就完了

多重背包

规定每种物品有 \(m_i\)

暴力解法

把每个物品看成一个独立的物品进行0/1背包

二进制优化多重背包

\(m_i\) 按2的倍数从小到大拆,最后是一个小于或等于最大倍数的余数,相当于1个k,2个k,4个k...这些物品进行0/1背包,便可组合出选 \(0~m_i\) 个k的情况,为什么呢,我们看一组例子

在这组例子中,相当于用 \(1k,2k,4k,3k\) 的组合方案来代替 \(0~10k\) 的组合方案,复杂度 \(O(C\sum_{i=1}^n\log_2m_i)\)

单调队列优化多重背包

复杂度更优,待填坑

最长公共子序列

\(dp[i][j]\) 表示序列 \(X_{0~i}\) 和序列 \(Y_{0~j}\) 的最长公共子序列长度
\(x_i==y_j\) 时:\(dp[i][j]=dp[i-1][j-1]+1\)
\(x_i!=y_j\) 时:\(dp[i][j]=max(dp[i-1][j],dp[i][j-1])\)
复杂度 \(O(N^2)\),不是最优解

最长上升子序列

\(dp[i]\) 为长度为i的最长上升子序列,最后一个数的大小
对于每一个 \(a[i]\) 找到最大一个 \(dp[j]<a[i]\) 使 \(dp[j+1]=min(dp[j+1],a[i])\)
这个过程可以用二分加速,复杂度 \(O(N\log_2N)\)

P2758 编辑距离

子问题:将 \(X_{1...i}\) 转换成 \(Y_{1...j}\) 的最小操作次数
状态设计: \(dp[i][j]\) 为将 \(X_{1...i}\) 转换成 \(Y_{1...j}\) 的最小操作次数
状转方程:
\(X_i==Y_i\) 时:$$dp[i][j]=dp[i-1][j-1]+1$$
\(X_i!=Y_i\) 时:$$dp[i][j]=max(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1$$
\(dp[i-1][j-1]\) :替换 \(X_i\)\(Y_i\)

\(dp[i][j-1]\) :删除 \(X_i\)

\(dp[i-1][j]\) :在i位置插入 \(Y_i\)

最小划分

给一个正整数组,把它分为s1,s2两部分,然后求最小的 \(|s1-s2|\)
考虑一部分划分越接近 \(sum/2\) 越优,然后0/1背包做就可以了

树形dp

树上做dp非常常见,因为树本身有子结构性质(树和子树)
一般解题思路:先把树转化为有根树(如果不连通的树,就加一个虚拟根,它连接所有孤立的树),然后在做dfs,递归到叶子节点,再一层层返回信息,就在这一步做dfs

P2015 二叉苹果树

定义状态 \(dp[u][j]\) 表示以节点u为根的子树上留j条边时

换根dp

写的比较好的博客

P3478 STA-Station

板子题,比较适合练手

二叉树做法

考虑左右节点的边的总数是一定的,所以我们枚举一个子树的边就可以了

void dfs(int u){
	for(int i=0;i<=num;i++){//num代表子树内边的数量
		dp[u][i]=max(dp[u][i],dp[lson][i],dp[rson][num-i]);
	}
}

多叉树做法

我们挨个子节点枚举,然后再枚举当前子节点v选的边数,剩下的就是1~v-1 的子节点保留的分叉数

void dfs(int u){
	for(auto v:b[u]){//v是u的子节点
		for(int i=num;i>=1;i--){//枚举要割几条边
			for(int j=0;j<=i;j++){
				dp[u][j]=max(dp[u][j],dp[u][i-j-1]+dp[v][j]+w[u]);//i-j-1因为连向v也算一条边
                //实际上是dp[u][v][j]=max(dp[u][v][j],dp[u][v-1][i-j-1]+dp[v][j]+w[u]压掉v一维所以枚举i时要倒叙枚举
			}
		}
	}
}

P1352 没有上司的舞会

典题,不多赘述

P2014 选课

树上背包板子题,我们设 \(dp[u][t]\) 在u子树内选t个点所得到的最大值
所以我们的我们的针对一个子树内进行操作,枚举所有的子节点,再枚举要在这个子树內选取的点的个数 \(t\),再枚举一个在这个子节点的子树內枚举的点的个数 \(j\)

然后最后答案统计就是 \(dp[u][t]=dp[v][j]+dp[u][t-j]\)

状压dp

应用背景以集合为状态,集合一般可以用二进制表示,用二进制的位运算处理
集合问题一般是指数复杂度的,例如:1.子集问题,设n个元素没有先后关系,那么一共有 \(2^n\) 个子集;2.排列问题,对所有n个元素进行全排列,共有 \(n!\) 个排列
状态压缩:主要就是dp的一种状态,与dp转移关系不大
位运算:\(a\&(a-1)\) 把a的最后一个1去掉

P10447 最短 Hamilton 路径

典题,每个点只能经过一次,所以只需要设 \(dp[s][j]\) ,s为哪些点已经访问过了状压的状态,j为现在在哪个点,状态转移方程显然

区间dp

先在小区间上进行dp得到最优解,然后再合并小区间的最优解求得大区间的最优解,解题时,先解决小区间的问题,再将小区间合并为大区间,合并操作一般是将两个相邻区间合并
注:合并顺序从小区间到大区间,因该先从小到大枚举区间的长度,递推出j在哪里

ybt题解

背包问题

T2:

考虑首先b中的元素a中一定有,其次b中的元素不能被a中的元素拼成

所以我们先把b数组排个序,然后多重背包转移此数能否被之前书数拼成即可

T3:

多重背包二进制优化板子题

T4:

多重背包,但是状态只有0/1之分

所以我们考虑可以用一种新奇的方式转移

我们用 \(f[i-a[i]]\) 来递推 \(f[i]\) ,若 \(f[i-a[i]]\) 是用了一些 \(a[i]\) 硬币转移过来的,所以它转移到 \(f[i]\) 是又用了一枚硬币才转移过来的,因为硬币个数有限制,所以我们不能无限制的转移下去

存一个 \(cnt[j]\) 表示转移到 \(j\) 共用了多少枚 \(a[i]\) 硬币,判断是否超出范围即可

考虑多重背包为什么不能这样做呢?

猜想:可能是因为多重背包的状态维护的最大价值并不具有转移性质,可能有一个较大的不是用\(a[i]\)来转移的也有可能,反正我不好说

问题暂留,等待各位大佬解答

T5:

注意到每个主件最多有两个附件(首先我没有注意到),并且主件必须先选,考虑选主件的过程相当于是一个0/1背包,而连带的附件又很少

所以将主件和附件依次搭配,作为4种情况,在背包时分讨即可

T6:

板子题,先进行一个0/1背包再进行完全背包即可

T7:

板子题

T8:

比较坑人的一题,首先考虑你的约束条件可以构成几个环,然后我们要求有多少种方案能使得开锁的箱子覆盖所有的环

我们设 \(dp[i][j]\) 表示开j个箱子覆盖i个环的方案数

考虑dp,我们枚举i,然后再枚举j,最后再枚举k表示覆盖i个环开k个箱子,然后转移即可

因为问的是方案,所以要乘一个组合数

注:题目特别坑的地方

考虑组合数,n=300一定爆了,开long long也不行

所以题解中用double存的

唐老师解释说是因为double存的只是一个不精确的数,类似于科学计数法的那种,只存前几位对答案影响大的几位,因为保留4位小数,所以靠后的整数位对答案没有精度影响

T8:

经典dp套路多一个状态就多加一维

\(dp[i][v1][v2][0/1]\) 表示选到i数用了v1块1卡,v2块2卡,0/1表示用没用赠送

首先考虑没有必选状态的转移,直接分类讨论,状转式子复杂,代码中有

然后如果有必选状态应该怎么办呢,我们直接截胡,相当于只有选择必选状态的状态才能从这一层i过去,具体实现就是先把所有状态设为-inf,然后能转移的就能更新,否则就不能

题解中的实现是将两种选或不选混在一起写,我选择先把状态必选的刨出来先进行转移用g1存储,不必选的存在g2

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=305,inf=1e9+5;
int v1,v2,n,cnt1,cnt2;
int dp[N][505][55][2];
struct gift{
	int p,v;
}g1[N],g2[N];
int main(){
	scanf("%d%d%d",&v1,&v2,&n);
	for(int i=1;i<=n;i++){
		int p,v,s;
		scanf("%d%d%d",&p,&v,&s); 
		if(s)  g1[++cnt1]={p,v};
		else  g2[++cnt2]={p,v};
	}
	for(int i=1;i<=cnt1;i++){
		for(int j1=0;j1<=v1;j1++){
			for(int j2=0;j2<=v2;j2++){
				dp[i][j1][j2][1]=dp[i][j1][j2][0]=-inf;
				dp[i][j1][j2][1]=max(dp[i-1][j1][j2][0]+g1[i].v,dp[i][j1][j2][1]);
				if(j1>=g1[i].p){
					dp[i][j1][j2][1]=max(dp[i-1][j1-g1[i].p][j2][1]+g1[i].v,dp[i][j1][j2][1]);
					dp[i][j1][j2][0]=max(dp[i-1][j1-g1[i].p][j2][0]+g1[i].v,dp[i][j1][j2][0]);
				}
				if(j2>=g1[i].p){
					dp[i][j1][j2][1]=max(dp[i-1][j1][j2-g1[i].p][1]+g1[i].v,dp[i][j1][j2][1]);
					dp[i][j1][j2][0]=max(dp[i-1][j1][j2-g1[i].p][0]+g1[i].v,dp[i][j1][j2][0]);
				}
			}
		}
	}
	// printf("%d\n",dp[cnt1][v1][v2][1]);
	for(int i=1;i<=cnt2;i++){
		for(int j1=0;j1<=v1;j1++){
			for(int j2=0;j2<=v2;j2++){
				dp[i+cnt1][j1][j2][0]=dp[i-1+cnt1][j1][j2][0];
				dp[i+cnt1][j1][j2][1]=dp[i-1+cnt1][j1][j2][1];
				dp[i+cnt1][j1][j2][1]=max(dp[i-1+cnt1][j1][j2][0]+g2[i].v,dp[i+cnt1][j1][j2][1]);
				if(j1>=g2[i].p){
					dp[i+cnt1][j1][j2][1]=max(dp[i-1+cnt1][j1-g2[i].p][j2][1]+g2[i].v,dp[i+cnt1][j1][j2][1]);
					dp[i+cnt1][j1][j2][0]=max(dp[i-1+cnt1][j1-g2[i].p][j2][0]+g2[i].v,dp[i+cnt1][j1][j2][0]);
				}
				if(j2>=g2[i].p){
					dp[i+cnt1][j1][j2][1]=max(dp[i-1+cnt1][j1][j2-g2[i].p][1]+g2[i].v,dp[i+cnt1][j1][j2][1]);
					dp[i+cnt1][j1][j2][0]=max(dp[i-1+cnt1][j1][j2-g2[i].p][0]+g2[i].v,dp[i+cnt1][j1][j2][0]);
				}
			}
		}
	}
	if(dp[n][v1][v2][1]<0)  printf("-1");
	else printf("%d",dp[n][v1][v2][1]);
}

T9

区间dp

T4:

啊啊啊为什么区间dp的题想根本想不出来啊啊啊

\(dp[i][j]\) 表示区间获得的最大价值是多少(注:不是将区间内全部删掉)

我们分情况讨论

考虑如果 \((i+1,j-1)\) 这段区间可以全部被删除,就考虑能不能将 \((i,j)\) 合并删除掉

如何判断 \((i+1,j-1)\) 这段区间可以全部被删除是否成立?

只要满足条件 \(dp[i+1][j-1]==\sum_{i=i+1}^{r-1} b[i]\) 即可(因为这一段可以全部删掉就自然可以获得这一段的价值总和)

因为正常的合并条件为 \(a[i]+a[j]<=k\),所以再次判断一下即可

如果 \((i+1,j-1)\) 这段区间不可以全部被删除

我们自然是将大区间拆分小区间的方法,就是正常区间dp做法,枚举断点k即可

T5:

推式子。。。

求最小值,经典操作,先把固定的东西丢掉

去掉根号

考虑用二维区间dp实现

这里有一点变式,因为这个不是从小区间推导到大区间,而是从前一次的状态推导到这一次,并不需要区间从小到大枚举,但是我们是倒推,从一个小矩形往边上加块,推得一个大区间,所以我们要把那些不合法还没有通过推导得到的状态设为inf

\(dp[i][j][x][y][k]\) 代表第k次转移,合并出来的块左上角为 \((i,j)\) 右下角为 \((x,y)\) 的最小代价,所以初始状态就是使 \(dp[i][j][x][y][0]\) 为这个块的权值,也可以理解为最后剩下来的块的大小

转移

这个是横着割竖着割不同的情况,枚举g,我们可以分别选择一个块s作为我们拼出来的块,另一个块t代表我们此次操作拼上去的块,所以本次的贡献既是 \(dp[s][k-1]+num[t]\) (num代表块的权值和)

代码

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=10;
int n;
int a[N][N],num[N][N],sum[N][N],dp[N][N][N][N][20];
int query(int i,int j,int x,int y){
	int cnt=sum[x][y]+sum[i-1][j-1]-sum[i-1][y]-sum[x][j-1];
	return cnt*cnt;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=8;i++){
		for(int j=1;j<=8;j++){
			scanf("%d",&a[i][j]);
			num[i][j]=num[i][j-1]+a[i][j];
			sum[i][j]=sum[i-1][j]+num[i][j];
		}
	}
	memset(dp,0x3f3f3f,sizeof(dp));
	for(int i=1;i<=8;i++){
		for(int j=1;j<=8;j++){
			for(int x=i;x<=8;x++){
				for(int y=j;y<=8;y++){
					dp[i][j][x][y][0]=query(i,j,x,y);
				}
			}
		}
	}
	for(int k=1;k<n;k++){
		for(int lenx=1;lenx<=8;lenx++){
			for(int i=1;i+lenx-1<=8;i++){
				for(int leny=1;leny<=8;leny++){
					for(int j=1;j+leny-1<=8;j++){
						int x=i+lenx-1,y=j+leny-1;
						for(int g=i;g<x;g++){
							dp[i][j][x][y][k]=min(dp[i][j][g][y][k-1]+query(g+1,j,x,y),dp[i][j][x][y][k]);
							dp[i][j][x][y][k]=min(dp[g+1][j][x][y][k-1]+query(i,j,g,y),dp[i][j][x][y][k]);
						}
						for(int g=j;g<y;g++){
							dp[i][j][x][y][k]=min(dp[i][j][x][g][k-1]+query(i,g+1,x,y),dp[i][j][x][y][k]);
							dp[i][j][x][y][k]=min(dp[i][g+1][x][y][k-1]+query(i,j,x,g),dp[i][j][x][y][k]);
						}
					}
				}
			}
		}
	}
	double xb=(double)sum[8][8]/(double)n;
	// printf("%lf\n",xb);
	printf("%.3lf",sqrt((double)dp[1][1][8][8][n-1]/(double)n-xb*xb));
}

T6:

为什么这么简单的题我都想不出来!!!

只能从两边删数可以看成从一个中心区间拓展到两个大区间的过程

我们可以对于区间 \((i,j)\) 分为两种情况

1.直接删去区间

2.分别删去
枚举k,正常转移即可

T7:

差一点就想到了啊啊啊啊啊啊啊
没有想起来区间dp要枚举k呜呜呜

贪心策略:
最有情况下,必定将一个狼一次性消灭,证明因为把一只狼消灭到一半再去攻击其他的狼受到伤害必定不小于一次性消灭掉的伤害

我们设 \(dp[i][j]\) 为删去 \((i,j)\) 受到伤害最小值

于是只要枚举攻击的狼k,受到的伤害为 \((b[i-1]+b[j+1]+a[k])*c[i]\)\(c[i]\) 为受到攻击的次数)

转移式子

for(int k=i;k<=j;k++){
		dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+(a[k]+b[i-1]+b[j+1])*h[k]);
}

注意初始化时要把所有状态设为inf

然后对于 \(dp[i][i]=(a[i]+b[i-1]+b[i+1])*c[i]\)

注意因为我的代码还有一点比较特殊就是会访问到 \(dp[i+1][i],dp[i][i-1]\) 所以将这些赋值为0即可

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=405;
int n,atk;
int a[N],b[N],h[N],dp[N][N];
int main(){
	scanf("%d%d",&n,&atk);
	for(int i=1;i<=n;i++){
		scanf("%d%d%d",&a[i],&b[i],&h[i]);
		h[i]=(int)ceil((double)h[i]/(double)(atk));
	}
	memset(dp,0x3f3f3f3f,sizeof(dp));
	for(int len=1;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			if(len==1){
				dp[i][j]=(a[i]+b[i-1]+b[i+1])*h[i];
				dp[i][i-1]=dp[i+1][i]=0;
				continue;
			}
			for(int k=i;k<=j;k++){
				dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+(a[k]+b[i-1]+b[j+1])*h[k]);
			}
		}
	}
	printf("%d",dp[1][n]);
}

T8:

我们把一整块都为白色的贡献设为0,否则设为 \(\max(n,m)\)

然后二维区间dp转移即可

T9:

\(dp[i][j]\) 为男生选i,女生选j,所获取到的最大愉悦度,至于为什么这个dp没有后效性。。。我也不知道。。。

然后我们可以枚举上一维是由选哪两个人,得来的,可以枚举不选前一段男生或不选前一段女生的长度,具体转移式子可以参考ybt

这里为什么可以只枚举删去一段连续的女生或男生而不是两边都删呢,我们会发现这因该是包不优的,因为我们可以在两边都不选的一堆男生和女生中插进来一对,一定比之前更优

这里我之前有一个问题,就是这里为什么要选取边界条件设为-inf呢

for(int i=1;i<=n+1;i++){
	dp[i][0]=dp[0][i]=-inf;
}

考虑首先在定义上它就不合法, \((0,0)\) 一定要配成一对,然后第0号男生女生就不可以再配对其余男生或女生了,其次,如果说它可以这样转移的话,会有一部分算不上贡献,如下

最终代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=305,inf=1e18+5;
int n;
int a[N],suma[N],sumb[N],b[N],dp[N][N];
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		suma[i]=suma[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		scanf("%lld",&b[i]);
		sumb[i]=sumb[i-1]+b[i];
	}
	suma[n+1]=suma[n];
	sumb[n+1]=sumb[n];
	for(int i=1;i<=n+1;i++){
		dp[i][0]=dp[0][i]=-inf;
	}
	for(int i=1;i<=n+1;i++){
		for(int j=1;j<=n+1;j++){
			dp[i][j]=dp[i-1][j-1]+a[i]*b[j];
			for(int k=1;k<i;k++){
				dp[i][j]=max(dp[k-1][j-1]+a[i]*b[j]-(suma[i-1]-suma[k-1])*(suma[i-1]-suma[k-1]),dp[i][j]);
			}
			for(int k=1;k<j;k++){
				dp[i][j]=max(dp[i-1][k-1]+a[i]*b[j]-(sumb[j-1]-sumb[k-1])*(sumb[j-1]-sumb[k-1]),dp[i][j]);
			}
		}
	}
	printf("%lld",dp[n+1][n+1]);
}

数位dp

T3:

应该先从这道题入手数位dp,首先理解数位dp,dp了什么东西

它是把一个数从低位拆到高位,相当于是较低位的方案数是较高位的子问题

个人认为数位dp写记忆化搜索更容易理解一点

看这道题,首先我们让我们分别求 \(0~9\)\(l~r\) 出现的次数,我们先把 \(0~9\) 分别求,再把 \(l~r\) 差分一下,就是用 \(1~r\) 减去 \(1~l-1\)

编码时,我们用now来存储统计0~9中的一个数字,下面以now=2为例

我们存一下 \(dp[pos][sum]\) 表示最后pos位,pos位前面前面有sum个数=now

举个例子:

然后我们下传时要用lead表示是否有前导0,limit表示当前最高位是否有数位限制

个人理解需要下传是否有前导0的原因是因为我们要在统计0出现的个数,而不能把前导0也给统计上,此处仅为本人个人想法,如有纠正,请发表评论

ps:已经证明了,我将所有lead删去之后,其余数位全部正确,只有统计0的个数时出现错误了,说明下传前导0,只与统计0时不能统计上前导0有关

记搜上代码(更容易理解一些):

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=20;
int l,r,now;
int num[N],dp[N][N];
int dfs(int pos,int sum,bool lead,bool limit){
    int ans=0;
    if(pos==0)  return sum;//说明已经精确到一个数了,直接返回这个数中的now个数
    if(!lead&&!limit&&dp[pos][sum]!=-1)  return dp[pos][sum];//因为我们记忆化的是没有前导0,没有限制的大众情况
    int up=9;
    if(limit)  up=num[pos];//up表示这一位最高到哪里,没有限制就是9
    for(int i=0;i<=up;i++){
        if(i==0&&lead)  ans+=dfs(pos-1,sum,1,limit&&i==up);//下传前导0情况
        else if(i==now)  ans+=dfs(pos-1,sum+1,0,limit&&i==up);//这一位出现了now
        else  ans+=dfs(pos-1,sum,0,limit&&i==up);
    }
    if(!lead&&!limit)  dp[pos][sum]=ans;//记忆化搜索
    return ans;
}
int solve(int x){
    int len=0;
    while(x){
        num[++len]=x%10;
        x/=10;
    }
    memset(dp,-1,sizeof(dp));
    return dfs(len,0,1,1);
}
signed main(){
    scanf("%lld%lld",&l,&r);
    for(int i=0;i<=9;i++){
        now=i;
        printf("%lld ",solve(r)-solve(l-1));
    }
}

T1:

就这题还放T1,它什么难度,你心里没点B数吗

B数要满足两个限制,一个是要出现13,另一个是要被13整除

也就是说我们只有满足这个条件才能被统计到,就是转化一下,T3是出现now可以被统计,我们这个只不过是更复杂了一些,所以判断时也更复杂,但是没有本质区别

对于除以13的余数这个玩意,考虑从上一位下传到下一位,是把上一位除以13得到的余数*10再加上这一位的数在模上13,就是这一位剩下的余数,所以我们下传一个这一位的余数y

对于是否包含13这个限制我们设三种状态,0没有任何包含,1上一位是1,2已经包含13,然后我们根据这个状态转移就好了

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=20;
int n;
int num[N],dp[N][N][N];
int check(int x,int c){
    if(c==2)  return 2;//注(卡了我好久):只要有13,就可以算,所以放在最前面
    if(x==1)  return 1;
    if(c==1&&x==3)  return 2;
    return 0;
}
int dfs(int pos,int y,int c,int limit){
    int ans=0;
    if(!pos)  return !y&&c==2;//判断这个数是否位B数
    if(!limit&&dp[pos][y][c]!=-1)  return dp[pos][y][c];///记搜
    int up=9;
    if(limit)  up=num[pos];
    for(int i=0;i<=up;i++){
        ans+=dfs(pos-1,(y*10+i)%13,check(i,c),limit&&i==up);
    }
    if(!limit)  dp[pos][y][c]=ans;
    return ans;
}
signed main(){
    while(scanf("%lld",&n)!=EOF){
        int len=0;
        while(n){
            num[++len]=n%10;
            n/=10;
        }
        memset(dp,-1,sizeof(dp));
        printf("%lld\n",dfs(len,0,0,1));
    }
    return 0;
}

T2:

切了,和T1思路基本一样,先把n转化为二进制,只需要下传二进制数位0/1的个数即可,注意也要处理前导0情况不能算

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=50;
int l,r;
int num[N],dp[N][N][N];
int dfs(int pos,int sum0,int sum1,int lead,int lim){
    if(!pos)  return sum0>=sum1;
    if(!lead&&!lim&&dp[pos][sum0][sum1]!=-1)  return dp[pos][sum0][sum1];
    int up=1,ans=0;
    if(lim)  up=num[pos];
    for(int i=0;i<=up;i++){
        if(i==0&&lead)  ans+=dfs(pos-1,sum0,sum1,1,lim&&i==up);
        else if(i==0)  ans+=dfs(pos-1,sum0+1,sum1,0,lim&&i==up);
        else  ans+=dfs(pos-1,sum0,sum1+1,0,lim&&i==up);
    }  
    if(!lead&&!lim)  dp[pos][sum0][sum1]=ans;
    return ans;
}
int solve(int x){
    int len=0;
    while(x){
        num[++len]=x%2;
        x/=2;
    }
    memset(dp,-1,sizeof(dp));
    return dfs(len,0,0,1,1);
}
signed main(){
    scanf("%lld%lld",&l,&r);
    printf("%lld",solve(r)-solve(l-1));
}

ps:此题如果wa了的话,会发现显示数据是错误的,但是wa了跟数据无关,如果是90分可以看一下是不是空间开小了

T5:

正确的做法lz懒得写,直接二分切了

我们显然容易判断 \(1~x\) 中幸运数字的个数,然后就拿这个二分一下就可以做了

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60,M=1e10;
int T,n;
int dp[N][4],num[N];
int pan(int x,int c){
    if(c==3)  return 3;
    if(x==6)  return c+1;
    return 0;
}
int dfs(int pos,int c,bool lim){
    if(!pos)  return c==3;
    if(!lim&&dp[pos][c]!=-1)  return dp[pos][c];
    int up=9,ans=0;
    if(lim)  up=num[pos];
    for(int i=0;i<=up;i++){
        ans+=dfs(pos-1,pan(i,c),lim&&i==up);
    }
    if(!lim)  dp[pos][c]=ans;
    return ans;
}
int check(int x){
    int len=0;
    while(x){
        num[++len]=x%10;
        x/=10;
    }
    memset(dp,-1,sizeof(dp));
    return dfs(len,0,1);
}
signed main(){
    scanf("%lld",&T);
    while(T--){
        scanf("%lld",&n);
        int l=666,r=M;
        while(l<r){
            int mid=(l+r)/2;
            if(check(mid)>=n)  r=mid;
            else  l=mid+1;
        }
        printf("%lld\n",l);
    }
}

T6:

直接切了

最板子的数位dp,然后把ans+=,改为ans*=就过了,最后再判断掉sum(0)的情况就行了

写完后喵了一眼题解,不懂啊为什么要用快速幂,既复杂又慢,像个小丑🤡

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60,mod=1e7+7;
int n;
int dp[N][N],num[N];
int dfs(int pos,int sum,bool lim){
    if(!pos)  return sum;
    if(!lim&&dp[pos][sum]!=-1)  return dp[pos][sum];
    int up=1,ans=1;
    if(lim)  up=num[pos];
    for(int i=0;i<=up;i++){
        if(sum==0&&pos==1&&i==0)  continue;//特判0的情况
        ans*=dfs(pos-1,sum+(i==1),lim&&i==up);//加改为乘
        ans%=mod;
    }  
    if(!lim)  dp[pos][sum]=ans;
    return ans;
}
signed main(){
    scanf("%lld",&n);
    int len=0;
    while(n){
        num[++len]=n%2;
        n/=2;
    }
    memset(dp,-1,sizeof(dp));
    printf("%lld",dfs(len,0,1));
}

树形dp

T5:

神奇的一种转化,经典思路

考虑一条边对答案贡献了多少次

把用一条边把树分成两个部分,因为其是树边,所以一部分必然是一个端点的子树,另一部分是除去这个子树外的其余所有树,因为其说是至少一个端点是叶子节点,所以我们只要一部分一个叶子节点和另一部分的任意一个点组合即可

只需要统计一下该子树内的叶子节点和节点个数,然后另一部分的用总的减去该子树內的容斥即可

T6:

一个人走的情况就是求一下s能到达的最长距离,两个人的情况就是求一下树上最长链

额额额,手模出来的,再加上一些感性理解

树上最长链,可以求一下由这个点出发能到达这个子树的最远距离,然后最长链自然是用这个距离再加上另一条和这条路径不重复的最长路径,也就是在我们树形dp转移时统计一下最长和次长的距离即可(注意不可交,就是用其子节点转移即可)

注意,s能到的最长路径不能直接用你求树上最长链的答案,因为它是子树內的最长路径

T7:

巧妙地推式子题,考虑首先设我们以 \(i\) 为根时,包含 \(i\) 所构成的连通块并且满足有特殊值的方案数,这个是不好做的

考虑容斥,我们设 \(f[i]\) 为包含 \(i\) 所构成的连通块的总方案数,\(g[i]\) 为包含 \(i\) 所构成的连通块中不包含特殊值的方案数,最终答案即为 \(ans[i]=f[i]-g[i]\)

然后怎么求 \(f[i],g[i]\)

对于 \(f[i]=\prod_{j\in son[i]} (f[i]+1)\) 为什么呢,对于每一个子树都有 \(f[j]\) 种选的方案,还有一种不选 \(j\) 的方案

对于 \(g[i]\) 若其不为特殊点,则转移方式和 \(f[i]\) 一样,否则 $g[i]=0 $

然后注意因为是取完模之后,所以在求 \(ans[i]\) 的时候别忘了先加一个模数再取模

状压dp

T1:

几种状压dp常见优化方式:
1.先把行内合法情况预处理出来
2.可以用& O(1) 比较两个串的基本情况

T3:

三进制我们先预处理出所有本位合法的情况,再预处理出行间两个状态比较是否合法这样可以优化掉一些复杂度

然后按照三进制存状态和转移即可

状压的精髓就在于,不需要两个状态按位比较,如果两个状态没法 \(O(1)\) 判断是否合法,也就失去了状压的意义

注:写题时,不小心把状态由0开始写成了从1开始,但是竟然拿到了90pts,我当时很惊讶,但是一想,是因为当m!=1时,0状态是不合法的

T4:

我们存一下上两位的状态进行转移,然后枚举第i位,i-1位,i-2位判断合法并转移,转移的话,预处理出本位合法状态和每种状态对应的1的个数即可

观察复杂度,\(2^10=1024\) 你会发现:啊?\(2^{m^3}n\),跑满的话 \(1e11\)

但是你会发现有些情况并不合法,考虑本位合法情况

你可用一个递推来求出合法情况 \(f[i]=f[i-1]+f[i-3]\)

同时也可以采用打表的方式,你会发现,就算所有位置都可以摆炮兵,最多也只有60种合法情况!

最终复杂度 \(O(60^3*100)=O(2e7)\),轻松跑过

T5:

之前见过相似的trick,但还是没有考虑的很全面

我们考虑只需要通过一个状态的子集转移过来即可,所以对于对于每一种状态暴力枚举子集,具体方法就是 \(j=i\&(j-1)\),枚举所有的j就是i的子集,然后复杂度是 \(3^n\) 我之前证明过,忘了怎么证了。。。

T6:

先预处理出所有关键点+起点到任意点的距离,然后就很简单了,对关键点跑一遍旅行商问题即可

T7:

考虑设我们经过的点总和s1,边的总和s2,我们最终让s1/s2最小

然后我们考虑一件事情,就是选哪些点确定了,s1就确定了,我们让s2最大化就可以了

所以状压还是存一下选哪些点,然后存的是选这些点能获得的边权最大值,然后注意最开始要将所有状态赋值位-inf,只有起点为0即可,因为这样能保证只能从起点出发

T8:

考虑我们用二进制存一下删掉哪些字母,然后我们就现预处理出每一个字串是否为回文串,然后我们枚举每一种状态,再枚举其子集,转移即可

单调队列优化dp

T2:

再也不想碰这道题了。。。写了一下午。

我们先设状态 \(dp[i][j]\) 表示前i个刷匠,考虑了前 \(j\) 个木板后所获得的最大价值(\(j\) 个木板可以有空余)。

然后枚举前 \(i\) 个刷匠,枚举每一条木板。

对于一条木板可以此刷匠根本不刷,或不刷当前木板,状转方程:

\[dp[i][j]=max(dp[i-1][j],dp[i][j-1]) \]

还可以刷以这一块木板为结尾的连续一段区间,这个转移条件为 \(j>=s[i]\)\(j-l[i]+1<=s[i]\),我们把这一段要刷的区间设为 \([k+1,j]\) 状转方程

\[dp[i][j]=\max^{k<s[i]}_{k=j-l[i]} \{dp[i-1][k]+p[i]*(j-k)\} \]

\(p[i]*j\) 是固定的我们可以把它提出来

\[dp[i][j]=p[i]*j+\max^{k<s[i]}_{k=j-l[i]} \{dp[i-1][k]-p[i]*k\} \]

后面这一堆我们可以用单调队列维护,注意到因为单调队列右端点是固定的 \(s[i]\),所以我们可以先预处理出单调队列然后维护单调队列的头即可。

注意一些边界条件特判。

T3:

比较巧妙的优化,因为注意到我们是从i的前k个dp数组中转化dp[i]的,所以单调队列来维护前k位dp最小值,但是,你会发现如果前面的高度小于此树高,就会带来1的贡献,但是它只有1啊,所以我们只需要让高度小的在单调队列前即可

注意:树高是严格小于

T4:

时隔许久,我终于来填单调队列这一道大坑了

现在看来也没有那么难

by the way,这道题题目描述错了,输入时b,t反了,原题为CF372C

首先我们枚举烟花,从时间从大到小的顺序,然后再枚举在哪个位置转移的话,就是从 \(t_i~t_{i-1}\) 这段时间能到这个位置的所有位置都可以转移到这,然后这个转移可以用单调队列优化

T5:

首先注意题目中的几个小细节:要先从0号节点出发,也就是我们需要手动添加一个距离为0,权值为0的节点,其次是可以在任意位置结束,就是只要存在dp值大于k就可以了

很明显这题一眼二分,然后我们根据二分出的g来确定单调队列的范围,然后转移即可

注:细节若一个点无法从原先的点转移过来,就把这个点的dp设为-inf

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5,inf=1e18;
int n,d,k,mx,q1,q2;
int u[N],w[N],q[N],dp[N];
void add(int x,int z){
    while(q1<=q2&&u[q[q1]]<z)  q1++;
    while(q1<=q2&&dp[q[q2]]<=dp[x])  q2--;
    if(u[x]<z)  return;
    q[++q2]=x;
}
bool check(int g){
    for(int i=0;i<=n;i++)  dp[i]=0,q[i]=0;
    dp[0]=0;
    u[0]=0;
    q1=1,q2=0;
    int l=max(d-g,1ll),r=d+g;
    for(int i=1,j=0;i<=n;i++){
        while(u[j]<=u[i]-l){
            add(j,u[i]-r);
            j++;
        }
        while(q1<=q2&&u[q[q1]]<u[i]-r)  q1++;
        if(q1>q2){
            dp[i]=-inf;
            continue;
        }
        dp[i]=dp[q[q1]]+w[i];
        if(dp[i]>=k)  return 1;
    }
    return 0;
}
signed main(){
    scanf("%lld%lld%lld",&n,&d,&k);
    for(int i=1;i<=n;i++){
        scanf("%lld%lld",&u[i],&w[i]);
        mx=max(u[i],mx);
    }
    int l=0,r=mx;
    while(l<r){
        int mid=(l+r)>>1;
        if(check(mid))  r=mid;
        else  l=mid+1;
    }
    printf("%lld",l);
}

T6:

本质上是单调栈,我们维护一个递增的单调栈,然后假如i元素被j元素弹出,说明i元素的一个边界为j元素,所以正反跑一遍单调栈,统计左右边界

T7:

板子题,然后记录一下从哪里转移过来的就好了

T8:

二分单调队列长度,板子

T9:

我们设 \(dp[i]\)\((1,i)\) 不包含违禁词的最小减少量,然后我们设 \(f[i]\) 为以 \(i\) 结尾,不包含违禁词的区间的开头,注意:只有满足有单词以 \(i\) 结尾,才进行转移,所以转移就是 \(dp[i]=\max ^i_{k=f[i]}\{ dp[i-1]+a[i] \}\)

然后我们对每一个单词跑一遍kmp,然后预处理出 \(f[i]\) ,只有有值才进行转移,转移范围就是 \(f[i]\),用单调队列维护即可

T10:

毒瘤题,题解不可看

朴素dp: \(f_i=\min\{f_j+\max_{k=j+1}^i a_k\}\)\(j\) 满足 \(\sum^i_{j=x} a_x<m\)

我们先预处理对于每一个 \(i\)\(j\) 的范围为 \(f_i\),然后对于这个范围我们可以维护一个单调递减的队列,这样就去掉了内层的 \(\max\) 限制,考虑如何拆掉第二层限制,我们观察 \(f_i\) 的性质,发现它是单调递增的,我们考虑我们的

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+5;
int n,m,q1,q2;
int a[N],sum[N],f[N],q[N],num[N],dp[N];
priority_queue<pii,vector<pii>,greater<pii> >mn,del;
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        if(a[i]>m){
            printf("-1");
            return 0;
        }
    }
    f[0]=1;
    for(int i=1;i<=n;i++){
        sum[i]=sum[i-1]+a[i];
        f[i]=f[i-1];
        while(sum[i]>m){
            sum[i]-=a[f[i]];
            f[i]++;
        }
        // printf("%d ",f[i]);
    }
    // printf("\n");
    q1=1,q2=0;
    for(int i=1;i<=n;i++){
        while(q1<=q2&&q[q1]<f[i]){
            del.push({num[q[q1]],q[q1]});
            q1++;
        }
        while(q1<=q2&&a[q[q2]]<=a[i]){
            del.push({num[q[q2]],q[q2]});
            q2--;
        }
        q[++q2]=i;
        if(q1==q2)  num[i]=dp[f[i]-1]+a[i];
        else  num[i]=dp[q[q2-1]]+a[i];
        mn.push({num[i],i});
        del.push({num[q[q1]],q[q1]});
        num[q[q1]]=dp[f[i]-1]+a[q[q1]];
        mn.push({num[q[q1]],q[q1]});
        while(!del.empty()){
            if(del.top()!=mn.top())  break;
            del.pop(),mn.pop();
        }
        if(i!=1)  dp[i]=mn.top().first;
        else  dp[i]=a[i];
        // printf("%d %d %d\n",dp[i],num[i],mn.top().second);
        // for(int j=q1;j<=q2;j++){
        //     printf("%d ",q[j]);
        // }
        // printf("\n");
    }
    printf("%lld",dp[n]);
    return 0;
}
posted @ 2024-11-12 21:07  daydreamer_zcxnb  阅读(28)  评论(0编辑  收藏  举报