动态规划学习笔记(持续更新)

什么是动态规划?

股民四姨(
动态规划就是在持续遍历的过程中总结先前的最优答案
再通过先前的的答案更新出下一步的策略
最后总结所有答案得出问题的最优解

总的来说,看到以下成分基本就可以推断出这是一道动态规划题:
1,无后效性,即先前的决策不会影响到后面决策的正确性,下一步决策可用同种方法求出)
也就是说,我可以通过先前的解法最优解推断出下一步的最优解(或下一步的答案是多个先前答案的并)
先前的决策也会影响到下一步该如何决策(否则就是贪心问题)
2,可取最优性,即所有先前的方案能够总结出最优解,以便推断出下一步(或可并性)
3,复杂度过关,若求出的解法时间复杂度不过关,那么要么是没想到优化方案,审清楚题目的特殊条件进行优化(或者这道题压根不是动态规划)

动态规划的具体实现

一般而言,一道动态规划题需要一个状态转移方程
具体格式如下:
f ( i ) = ( f ( i − j ) . . . . . . . , f ( i ) ) f(i) = (f(i-j)....... ,f(i)) f(i)=(f(ij).......,f(i))
当然,这只是举个例子,一般的状态转移方程不会这么简单
相对的,f函数也不一定只有i这一个参数,到后面可能会出现形如f(i,j)或f(i,j,k)等更多维度的函数
还要注意好每个维度的值对应的代表意义和整个函数的值代表着什么,从而列出方程解决问题

daka,动态规划问题本身并不难,主要难的是如何通过题目推断出状态转移方程和状态数组
要想彻底掌握动态规划问题,还得多练,多积累,多总结,才能练到炉火纯青的地步

题题题题题题题题题题题题题题题题题题题题题题

入门题

最长上升子序列 (LIS)
最长上升子序列
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
仔细分析题目,我们发现,对于序列这个序列而言,如果区间 [ 1 , i ] [1,i] [1,i]的最长上升子序列的最后一个值小于等于 a [ i + 1 ] a[i+1] a[i+1]
那么区间 [ 1 , i + 1 ] [1,i+1] [1i+1]的最长上升子序列的长度就是区间 [ 1 , i ] [1,i] [1,i]的最长上升子序列的长度+1
但是问题来了
就比如下面这个序列:
1 5 6 2 3 4 7
如果按照我们先前那个算法的话,最后求出来的长度是4
最长上升子序列是 1 5 6 7
正解!



baka!仔细想想,最后求出来的长度应该是5
最长上升子序列是 1 2 3 4 7
仔细想想,当我们遍历到2这个数时,它还能跟前面的1这个数构成一个上升子序列1 2
遍历到3这个数时,摆在它面前的有三个序列: [ 1 , 5 ] [ 1 , 6 ] [ 1 , 2 ] [ 1 ] [1,5] [1,6][1,2][1] [1,5][1,6][1,2][1]
但是对于3这个数而言,它只能选择接在 [ 1 , 2 ] [1,2] [1,2]这个序列的后面,构成一个更长的上升子序列
考虑一下贪心,我们可以看到对于一个数而言,如果以他作为一个上升子序列的结尾,那么以他为结尾的他能构成的最长的上升子序列的长度一定等于位于它前方的结尾的数字比他小的且以它为结尾能够成的最长上升子序列的长度加1
是不是有点蒙?
举个例子
序列1 6 2 3 5 8
当我们遍历到5这个数字时,前面的选择有3个序列: [ 1 ] [ 1 , 6 ] [ 1 , 2 ] [ 1 , 2 , 3 ] [1][1,6][1,2][1,2,3] [1][1,6][1,2][1,2,3]
我们发现对于5而言,它无法接到序列 [ 1 , 6 ] [1,6] [1,6]的后面,因为6比5大
而对于其他的序列而言,因为接上去后结尾的数字一定是5,所以无论怎么接它们对下一步的影响都是相同的,换言之,不会对下一步决策产生影响
所以选择序列 [ 1 , 2 , 3 ] [1,2,3] [1,2,3]一定是最优的,这样才能保障选出来的区间最长子序列最长

总结消化下,最后得出的状态转移方程为 f ( i ) = m a x ( f ( j ) + 1 ∣ a ( j ) < = a ( i ) , f ( i ) ) f(i) = max(f(j)+1|a(j)<=a(i),f(i)) f(i)=max(f(j)+1∣a(j)<=a(i),f(i))
其中,f(i)表示以序列中的第i个数为结尾能够成的最长上升子序列
时间复杂度: O ( n 2 ) O(n^2) O(n2)
考虑优化
如果我们把 f ( i ) f(i) f(i)定义为长度为i的上升子序列的结尾的最小值
此时状态转移方程就变为了 f ( i ) = a ( j ) ∣ f ( i − 1 ) < = a ( j ) , f ( i ) > = a ( j ) f(i)=a(j)|f(i-1)<=a(j),f(i)>=a(j) f(i)=a(j)f(i1)<=a(j),f(i)>=a(j)
猜你想问:可是这时时间复杂度还是 n 2 n^2 n2啊?
不着急,仔细观察,我们会发现f数组一定满足单调递增性,因为对于任意一个j而言,根据状态转移方程, f ( j − 1 ) f(j-1) f(j1)一定小于等于 f ( j ) f(j) f(j)
所以每次找这个匹配值时,就可以使用二分查找优化(lower_bound()函数)
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

好惹,接下来是加强版:P1091 [NOIP2004 提高组] 合唱队形

请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
为什么说是加强版呢?
我们考虑到由于要组成一个山峰的形状
所以我们正反跑两次最长严格上升子序列(科普:严格上升子序列其实就是把前面的<=改为了<而已)
然后枚举中间那个峰的所在位置
此时需要踢出去的同学数量等于总同学的数量-左半边的最长严格上升子序列的长度-右半边的最长严格上升子序列的长度
取最小值即可
这也告诉我们一个道理:答案不一定是在动态规划数组里面的,有时候是用动态规划数组的值加以处理才能得到答案
(我知道这听起来很蠢但有时候总会陷入这样的思维盲区里·)

好惹,接下来是加强版中的加强版:P8816 [CSP-J 2022] 上升点列

请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
请先看完题目!!!
做题分步来~
我们暂且先不考虑添加点这个条件
仔细观察,我们发现,如果我们把每个点按照横坐标从小到大排序,若横坐标相同,则按纵坐标从小到大排序
那么这道题就变成了一个最长上升子序列问题,只不过将原先问题中的“编号大的节点只能接在编号小的节点后”这个条件转变为了“横坐标大的节点只能接在横坐标小的节点后”

问题来了,对于“可添加k个点”这个条件,我们应该怎么办呢?
因为前面的序列中添加过的节点数量也会影响到下一步添加能够添加的节点数量
也就是说,已经添加过的节点的数量会影响下一步决策
怎么办呢?
把它也一起存进数组里!
此时动态规划数组就变为了 f ( i , j ) f(i,j) f(i,j)
表示以第i个点为结尾,先前总共添加了j个节点,得到的最长子序列长度
状态转移方程: f ( i , j ) = m a x ( f ( i , j ) , f ( k , j − ( d i s ( i , k ) + 1 ) + d i s ( i , k ) ) f(i,j)=max(f(i,j),f(k,j-(dis(i,k)+1)+dis(i,k)) f(i,j)=max(f(i,j),f(k,j(dis(i,k)+1)+dis(i,k))
其中dis(i,k)表示从k走到i一共要走多少个单位长度
答案就是: a n s = m a x ( a n s , f ( i , j ) + k − j ) ans=max(ans,f(i,j)+k-j) ans=max(ans,f(i,j)+kj)

01背包问题


经典dp,设状态转移数组为 f ( i ) f(i) f(i)表示总体积为i时能够收获的最大价值
则状态转移方程为 f ( i ) = m a x ( f ( i ) , f ( i − j . t ) + j . v ) f(i)=max(f(i),f(i-j.t)+j.v) f(i)=max(f(i),f(ij.t)+j.v)
此时答案为 f ( m ) f(m) f(m)
猜你想问:那这样也不能保证一个物品只拿一遍啊啊啊啊啊啊啊
别急,上代码

...
for(int i=1;i<=n;i++)
{
	for(int j=m;j>=1;j--)
	{
		f[j]=max(f[j],f[j-t[i]]+v[i]);
	}
}
...

发现玄机没有?
因为我们枚举j的时候是倒序枚举的
所以这样每次取最大值的时候就不会取到 拿过一次当前物品的情况啦
时间复杂度: O ( n 2 ) O(n^2) O(n2)

上加强版!


事实上,这也宾不算是加强版
顶多算是转换了思路而已
对于这道题,我们发现它的m很大,v却很小
刚才的方法肯定行不通了
这时,我们转换下思路
还是那个熟悉的 f ( i ) f(i) f(i),只不过这时,它的含义变成了价值为i时所用体积和的最小值
状态转移方程变为 f ( i ) = m i n ( f ( i ) , f ( i − j . v ) + j . t ) f(i)=min(f(i),f(i-j.v)+j.t) f(i)=min(f(i),f(ij.v)+j.t)
此时答案就变为了 a n s = m a x ( a n x , i ∣ f ( i ) < = m ) ans=max(anx,i|f(i)<=m) ans=max(anx,if(i)<=m)
然后就可以愉快的ac啦!





猜猜我为什么没有给时间复杂度?
仔细算算,i的最大值为 1 ∗ 1 0 4 1*10^4 1104
此时的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
总计算次数为 1 0 8 10^8 108
这么大的数,大概率过不了吧(流汗黄豆)
此时我们再优化下

卡卡常就过啦
要什么优化!
所以有时候,解决问题的方法不止一种

上加强版(真)


这个“求第i个物品一定不在背包中时的最大值该怎么做呢?”

联系上文,我们是不是讲过一道题叫“合唱队形”?
想起来了8
同样的思路,我们可以从左往右跑一边背包,再从右往左跑一边背包,同时存储下来每次的状态
每次我们枚举那个不在的物品
暴力合并左右两边的最大价值即可
状态转移方程:不用再给了吧…
最后的答案就是: a n s = m a x ( a n s , f ( i − 1 , j ) + r f ( i + 1 , m − j ) ) ans=max(ans,f(i-1,j)+rf(i+1,m-j)) ans=max(ans,f(i1,j)+rf(i+1,mj))

附加题

接下来是zyb大佬提供的附加题:

因为gcd是可合并的,所以可以用线段树辅助合并
枚举不选的那个点,合并它左右两个区间的gcd
时间复杂度: O ( n l o g n l o g n ) O(nlognlogn) O(nlognlogn)
(当然还可以用st表,但是我懒)

这下棘手起来了,咋办?
联想到我们刚才的做法,维护一个类似前缀和的数组 f ( i ) f(i) f(i)表示区间[1,i]的gcd
再维护一个后缀和的数组 r f ( i ) rf(i) rf(i)表示区间[i,n]的gcd
每次枚举不选的数即可
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)


这个故事警示我们:多做总结,多打博客

加强加强版


仔细观察,我们发现,无论怎么交换,能对答案产生影响的只有交换有外挂的电脑的操作
很容易想到建图搜索

我来击碎你那不切实际的幻想!
最坏时间复杂度: n 3 n^3 n3

怎么办呢?
还是老套路
我们发现,能影响到答案的因素有两个:操作时间和此时纪狗电脑的位置
答案是最少拒绝操作次数
显而易见,对于在第u个位置的电脑而言,当他到达最小代价时,若操作为u,v交换,那么v此时的最小代价可能等于u的最小代价(同意交换操作)也有可能等于v过往的最小代价加1(不同意操作,代价加1)
所以设数组 f ( i ) f(i) f(i)表示电脑移到第i个位置所需的最小代价
则状态转移方程:
f ( u ) = m i n ( f ( u ) + 1 , f ( v ) ) f ( v ) = m i n ( f ( v ) + 1 , f ( u ) ) f(u)=min(f(u)+1,f(v)) \\ f(v)=min(f(v)+1,f(u)) f(u)=min(f(u)+1,f(v))f(v)=min(f(v)+1,f(u))
注意: f ( u ) f(u) f(u) f ( v ) f(v) f(v)必须同时取,即f(u)的状态转移方程中的f(v)必须是上一个时刻的f(v),不能是使用了状态转移方程后得到的f(v)
取f(v)的最新值时也同理

总结:有时候状态转移时不只调用一个状态转移方程,要根据情况而定

完全背包问题

上模板!

我们会发现,完全背包问题其实也只不过是在01背包的基础上添加了“所有物品都能取出无数次”的条件
所以我们可以在原先01背包的基础上,对每个物品跑多遍01背包,直到不存在更优解了再过渡到下一个物品
正解!



时间复杂度: O ( n 3 ) O(n^3) O(n3)
肯定不是正解啊
回想当初我们为什么要从体积大的循环到体积小的
那是为了保障每个物品只取一次呀!
现在我们每个物品能取无数次了
那我们干脆就反着来,从体积小的遍历到体积大的
这样体积大的的最优解就依据体积小的得出,物品就可以被计算多次
时间复杂度: O ( n 2 ) O(n^2) O(n2)
代码:(其实差不多多一样的啊)

...
for(int i=1;i<=n;i++)
{
	for(int j=t[i];j<=m;j++)
	{
		f[j]=max(f[j],f[j-t[i]]+v[i]);
	}
}
...

事实上,你可以把完全背包问题理解为01背包问题的一种特殊形式,这样就可以辅助理解

计数型dp

顾名思义,就是通过dp来统计数量的题目(

加强强强版



很容易想到记录一个动态规划数组 f ( i ) f(i) f(i)表示价值为i时的总方案数(不包括硬币数目超出的方案)
然后分别跑 d ( i ) d(i) d(i)次01背包问题
正解!


时间复杂度 O ( n 2 d i ) O(n^2di) O(n2di),差不多就是十的十一次方次运算
跑到明年都跑不完!
怎么办呢?

我们更改定义, f ( i ) f(i) f(i)表示价值为i时的总方案数(包括硬币数目超出的方案
得到状态转移方程 f ( i ) = f ( i − c 1 ) + f ( i − c 2 ) + f ( i − c 3 ) + f ( i − c 4 ) f(i)=f(i-c1)+f(i-c2)+f(i-c3)+f(i-c4) f(i)=f(ic1)+f(ic2)+f(ic3)+f(ic4)
我们发现,对于一个价值为i的方案而言,它想组成长度为i+k且包含原先方案的总方案数为 f ( k ) f(k) f(k)
为什么?
例如此时 c 1 = 1 , c 2 = 2 c1=1,c2=2 c1=1,c2=2先不考虑c3,c4
一个总价值为3的方案(1,2)想再组合成总价值为5的方案(包含原先的方案):
f ( 2 ) f(2) f(2)的方案为
( 1 , 1 ) , ( 2 ) {(1,1),(2)} (1,1),(2)
总价值为5的方案就是:
( 1 , 2 ) ( 1 , 1 ) , ( 1 , 2 ) ( 2 ) {(1,2)(1,1),(1,2)(2)} (1,2)(1,1),(1,2)(2)
i i i进化到 i + k i+k i+k的过程相当于 i i i的方案接上一段 f ( k ) f(k) f(k)的方案

结合上面的证明,我们可以得知
因为 f ( s ) f(s) f(s)表示价值为i时的总方案数
则答案=总方案数-不合法的方案数
我们发现,对于c1而言,他的不合法的方案中一定包含>d1个面值为c1的硬币
所以不合法的方案=((d1+1)个面值为c1的硬币)接上(f(s-(d1+1)*c1)的方案)
所以c1的数量超出的不合法的方案数= f ( s − ( d 1 + 1 ) ∗ c 1 ) f(s-(d1+1)*c1) f(s(d1+1)c1)
同理可得c2,c3,c4不合法的方案数
但是我们发现,c1不合法的方案中可能还包含这c2不合法的方案
如果都减去的话就会减去重复的部分
造成答案偏小
同理还有c1,c2,c3不合法的交
c1,c2,c3,c4不合法的交等等情况
所以采用容斥原理即可

总结:有时候计数型dp可以考虑使用容斥原理解决,排除掉不合法的方案,最后就是合法的方案

加强强强强版

windy 有 N N N 条木板需要被粉刷。 每条木板被分为 M M M 个格子。 每个格子要被刷成红色或蓝色。
windy 每次粉刷,只能选择一条木板上一段连续的格子,然后涂上一种颜色。 每个格子最多只能被粉刷一次。
如果 windy 只能粉刷 T T T 次,他最多能正确粉刷多少格子?
一个格子如果未被粉刷或者被粉刷错颜色,就算错误粉刷。
包含一个整数,最多能正确粉刷的格子数。
30 % 30\% 30% 的数据,满足 1 ≤ N , M ≤ 10 , 0 ≤ T ≤ 100 1 \le N,M \le 10,0 \le T \le 100 1N,M10,0T100
100 % 100\% 100% 的数据,满足 1 ≤ N , M ≤ 50 , 0 ≤ T ≤ 2500 1 \le N,M \le 50,0 \le T \le 2500 1N,M50,0T2500

一眼不会()
仔细思考下
会影响到答案的因素有以下几个:
每条木板的染色情况
已经染色的次数
没了。

至少大体方向是对了,我们希望把木板染色问题变为一个01背包问题
我们希望得到一个方案使得每个木板染色后得到的正确粉刷的的总格子数最大
设状态转移数组 f ( i , j ) f(i,j) f(i,j)表示给前i个木板染完色之后,总共染色j次,得到的最大正确粉刷格子数
g ( i , j ) g(i,j) g(i,j)表示第i块木板,染了j次色后得到的最大正确粉刷格子数
则得到状态转移方程:
f ( i , j + k ) = m a x ( f ( i , j + k ) , f ( i − 1 , j ) + g ( i , k ) ) f(i,j+k)=max(f(i,j+k),f(i-1,j)+g(i,k)) f(i,j+k)=max(f(i,j+k),f(i1,j)+g(i,k))
现在问题就转化为了:如何求 g ( i , j ) g(i,j) g(i,j)?
我们发现对于每次涂色而言,它涂色的范围是一个区间
且涂色的顺序一定是按照粉,蓝,粉,蓝,粉…或蓝,粉,蓝,粉,蓝…
为什么?
因为如果有两个相邻的颜色相同的区间
那么他们一起图上一个颜色需要涂1次
他们分别图上同一个颜色反而需要涂两次
这肯定不优嘛!
你可能会问:如果中间有格子不涂色怎么办?
那就一起涂上
因为中间的格子要么涂蓝色,要么涂粉色,要么不涂色
如果中间涂蓝色更优的话,那就一定会分出一种新的涂色方式,不影响这一步
如果不涂色的话,那肯定没有两个一起涂更优

若前j个格子,涂了i次色的最优解已经求出,则前j+k个格子,涂i+1次色的最优解就是前面的最优解加上j+1到j+k这个区间涂一次色的最优解
所以得到状态转移方程:
g ( i , j + k , l ) = m a x ( g ( i − 1 , j + k , l ) , g ( i − 1 , j , l − 1 ) + m a x ( 区间内涂粉正确的格子数 , 区间内涂蓝正确的格子数 ) ) g(i,j+k,l)=max(g(i-1,j+k,l),g(i-1,j,l-1)+max(区间内涂粉正确的格子数,区间内涂蓝正确的格子数)) g(i,j+k,l)=max(g(i1,j+k,l),g(i1,j,l1)+max(区间内涂粉正确的格子数,区间内涂蓝正确的格子数))
其中,i表示前i块木板,j表示前j位已经图上色,l表示总共涂了l次色的最优解
上代码!

#include<bits/stdc++.h>
using namespace std;
const int Mmax=55,Tmax=26000;
int n,m,t;
int sum[Mmax][Mmax];
int dp[Mmax][Mmax][Mmax];
int f[Mmax][Tmax];
int main()
{
	scanf("%d%d%d",&n,&m,&t);
	char in;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%c",&in);
			if(in!='1' and in!='0')
			{
				j--;
				continue;
			}
			sum[i][j]=sum[i][j-1]+in-'0';	
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int l=1;l<=m;l++)
		{
			for(int j=1;j<=m;j++)
			{
				for(int k=0;k<j;k++)
				{
					dp[i][j][l]=max(dp[i][j][l],dp[i][k][l-1]+max(sum[i][j]-sum[i][k],j-k-sum[i][j]+sum[i][k]));
				}
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=t;j++)
		{
			for(int k=1;k<=m;k++)
			{
				f[i][j+k]=max(f[i][j+k],f[i-1][j]+dp[i][m][k]);
			}
		}
	}
	int ans=0;
	for(int i=1;i<=t;i++)
		ans=max(ans,f[n][i]);
	printf("%d",ans);
}
/*
3 6 3
001100
111111
000000
*/

题题题题题题题题题题题题题题题题题题题题题题!


对于所有测试点,保证 1 ≤ n ≤ 100 1 \leq n \leq 100 1n100 1 ≤ m ≤ 2000 1 \leq m \leq 2000 1m2000 0 ≤ a i , j < 998 , 244 , 353 0 \leq a_{i,j} \lt 998,244,353 0ai,j<998,244,353

一眼顶针,鉴定为不会做(
既然放在动归的笔记里,那就一定是动归的题目啦(
我们思考一下,如果没有取一半的这个限制的话,那么题目就变得相当简单了
答案就是每一种烹饪方法能做出的菜品和+1的乘积减一
接下来就是求如何求出非法的方案数了
仔细观察题目,那个"至多一半的菜"绝对别有用意
俗话说得好“存在即合理”
仔细思考一下,这也就说明了一个问题:
非法的方案中主要食材的数量超出一半的食材只会是一种食材
翻译一下:如果所有的菜品中,用了食材A的菜品数量大于一半,那么用了食材B,C,D…的菜品数量一定小于一半
也就是说:一种非法方案中,超出数量的食材只会有一种
所以我们可以枚举每种超出数量的食材,求得出非法方案数
假设枚举到使用了食材j的的菜品超出数量
对于一种烹饪方式而言,我们会发现,不使用这种烹饪方式,选择不是j的食材做饭,选择j食材做饭,他们的影响都是不一样的
同样,先前选择的是哪几种食材并不重要,重要的是选择了多少种食材选择了多少种材料为j的菜
所以我们可以设计出状态转移数组 f ( i , j , k ) f(i,j,k) f(i,j,k)表示考虑前i种烹饪方式,总共选择了j种食材,选择了k种材料为指定超出的一半的方案数·
要注意的是,我们会先枚举每种超出一半的食材是哪个,也就是说,枚举出来的值不同,f(i,j,k)的值也不是相同的
相当于进行了m次动态规划
此时我们就可以得出状态转移方程:
f ( i , j , k ) = f ( i − 1 , j , k ) + f ( i − 1 , j − 1 , k ) ∗ ( s u m ( i ) − a ( i , c o l ) ) + f ( i − 1 , j − 1 , k − 1 ) ∗ a ( i , c o l ) f(i,j,k)=f(i-1,j,k)+f(i-1,j-1,k)*(sum(i)-a(i,col))+f(i-1,j-1,k-1)*a(i,col) f(i,j,k)=f(i1,j,k)+f(i1,j1,k)(sum(i)a(i,col))+f(i1,j1,k1)a(i,col)
其中sum(i)表示烹饪方式为i的所有材料的方案数之和
col表示枚举出来超出一半的主要食材编号

解释一下:
f ( i − 1 , j , k ) f(i-1,j,k) f(i1,j,k)表示烹饪方式为i的菜完全不选,由上一步继承而来,方案完全一样的方案数
f ( i − 1 , j − 1 , k ) ∗ ( s u m ( i ) − a ( i , c o l ) ) f(i-1,j-1,k)*(sum(i)-a(i,col)) f(i1,j1,k)(sum(i)a(i,col))表示选择了非col的方案数
f ( i − 1 , j − 1 , k − 1 ) ∗ a ( i , c o l ) f(i-1,j-1,k-1)*a(i,col) f(i1,j1,k1)a(i,col)表示选择了col的方案数

此时的时间复杂度: O ( m ∗ n ∗ m ∗ m ) = O ( n m 3 ) O(m*n*m*m)=O(nm^3) O(mnmm)=O(nm3)
咋办,还是超了啊
不急,我们思考一下,首先枚举的m和n肯定是优化不了了
接下来就是枚举j和k的两个m
我们发现,我们需要关注的真的是选择了多少种食材选择了多少种材料为j的菜吗?
我们关注他们两个的动机是什么呢?
答:判断一个方案是否合法
选择了多少种材料为j的菜 大于 选择了多少种食材 的一半,那么这种情况就是非法的
换言之,若选择了多少种材料为j的菜 大于 选择了多少种材料不为j的菜 ,那么这种情况就是非法的
很好理解吧
如果我的数量比其他的都要多
那么我们的总和中,我一定比总和的一半要大
这不就把 m 2 m^2 m2变成 m m m了嘛!
我们每次只需要枚举他们之间的差值即可,而不是枚举j和k
此时状态转移数组就变为了 f ( i , j ) f(i,j) f(i,j)
表示考虑前i种烹饪方式中,选择了多少种材料为j的菜选择了多少种材料不为j的菜 的差值为j的方案数
状态转移方程: f ( i , j ) = f ( i , j − 1 ) + f ( i − 1 , j + 1 ) ∗ ( s u m ( i ) − a ( i , c o l ) ) + f ( i − 1 , j − 1 ) ∗ a ( i , c o l ) f(i,j)=f(i,j-1)+f(i-1,j+1)*(sum(i)-a(i,col))+f(i-1,j-1)*a(i,col) f(i,j)=f(i,j1)+f(i1,j+1)(sum(i)a(i,col))+f(i1,j1)a(i,col)
时间复杂度: O ( n m 2 ) O(nm^2) O(nm2)
上代码!

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
const int Maxn=400,Maxm=4000,Mod=998244353;
int f[Maxn][Maxn],a[Maxn][Maxm],tot[Maxn],ans[Maxn][Maxn],fin;
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%lld",&a[i][j]);
			tot[i]+=a[i][j];
			tot[i]%=Mod;
		}
	}
	ans[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		ans[i][0]=1;
		for(int j=1;j<=i;j++)
		{
			ans[i][j]=ans[i-1][j]+ans[i-1][j-1]*tot[i];
			ans[i][j]%=Mod;
		}
	}
	for(int c=1;c<=m;c++)
	{
		for(int i=1;i<=n;i++)
			for(int j=n-i;j<=i+n;j++)
				f[i][j]=0;
		f[0][n]=1;
		for(int i=1;i<=n;i++)
		{
			for(int j=n-i;j<=i+n;j++)
			{
				f[i][j]=(f[i-1][j]+f[i-1][j+1]*(tot[i]-a[i][c]+Mod)%Mod+f[i-1][j-1]*a[i][c]%Mod)%Mod;
			}
		}
		for(int i=1;i<=n;i++)
		{
			fin+=f[n][i+n];
			fin%=Mod;
		}
	}
	for(int i=1;i<=n;i++)
	{
		fin=(fin-ans[n][i]+Mod)%Mod;
	}
	printf("%lld",fin*(Mod-1)%Mod);
}

期望流动归

上题!寿司仙人
初看,一脸懵
???????????????????????????????????
它难道不会一直递归下去吗?????
如果这样的话
那它是怎么出现整数解的?????
不急
想想先,对于每碟Sushi,我们并不关心它所处的位置(因为每次选的Sushi是随机的,不会·影响答案)
所以我们就可以设状态转移数组 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)
表示有i碟有0个寿司,有j碟有1个寿司,有k碟有2个寿司,有l碟有三个寿司的情况,使得它把寿司吃完的期望
我们发现 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)可以由先前的状态推出
具体的,有下面四种情况:
1, d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)的情况转移到 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l),此时 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)加上 ( d p ( i , j , k , l ) + 1 ) ∗ i / n (dp(i,j,k,l)+1)*i/n (dp(i,j,k,l)+1)i/n // (i/n表示选择0的概率)
2, d p ( i + 1 , j − 1 , k , l ) dp(i+1,j-1,k,l) dp(i+1,j1,k,l)的情况转移到 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l),此时 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)加上 ( d p ( i + 1 , j − 1 , k , l ) + 1 ) ∗ j / n (dp(i+1,j-1,k,l)+1)*j/n (dp(i+1,j1,k,l)+1)j/n$ // (i/n表示选择1的概率)
3, d p ( i , j + 1 , k − 1 , l ) dp(i,j+1,k-1,l) dp(i,j+1,k1,l)的情况转移到 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l),此时 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)加上 ( d p ( i , j + 1 , k − 1 , l ) + 1 ) ∗ k / n (dp(i,j+1,k-1,l)+1)*k/n (dp(i,j+1,k1,l)+1)k/n$ // (i/n表示选择2的概率)
4, d p ( i , j , k + 1 , l − 1 ) dp(i,j,k+1,l-1) dp(i,j,k+1,l1)的情况转移到 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l),此时 d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)加上 ( d p ( i , j , k + 1 , l − 1 ) + 1 ) ∗ l / n (dp(i,j,k+1,l-1)+1)*l/n (dp(i,j,k+1,l1)+1)l/n$ // (以此类推)
问题来了,为什么用先前的状态加1呢?
我们考虑到期望的公式为概率贡献
那么当前这一步的贡献就等于上一步吃了几口加上1
因为这一步为上一步再吃一口得到的,所以要加1
而上一步吃了几口就相当于上一步要吃几口的期望(感性理解)
所以这一步的期望=这一步的概率
(上一步的期望+1)
综合起来,得
d p ( i , j , k , l ) = ( d p ( i , j , k , l ) + 1 ) ∗ i / n + ( d p ( i + 1 , j − 1 , k , l ) + 1 ) ∗ j / n + ( d p ( i , j + 1 , k − 1 , l ) + 1 ) ∗ k / n + ( d p ( i , j , k + 1 , l − 1 ) + 1 ) ∗ l / n dp(i,j,k,l)=(dp(i,j,k,l)+1)*i/n+(dp(i+1,j-1,k,l)+1)*j/n+(dp(i,j+1,k-1,l)+1)*k/n+(dp(i,j,k+1,l-1)+1)*l/n dp(i,j,k,l)=(dp(i,j,k,l)+1)i/n+(dp(i+1,j1,k,l)+1)j/n+(dp(i,j+1,k1,l)+1)k/n+(dp(i,j,k+1,l1)+1)l/n
把1提出来,可得
d p ( i , j , k , l ) = d p ( i , j , k , l ) ∗ i / n + d p ( i + 1 , j − 1 , k , l ) ∗ j / n + d p ( i , j + 1 , k − 1 , l ) ∗ k / n + d p ( i , j , k + 1 , l − 1 ) ∗ l / n + i / n + j / n + k / n + l / n dp(i,j,k,l)=dp(i,j,k,l)*i/n+dp(i+1,j-1,k,l)*j/n+dp(i,j+1,k-1,l)*k/n+dp(i,j,k+1,l-1)*l/n + i/n+j/n+k/n+l/n dp(i,j,k,l)=dp(i,j,k,l)i/n+dp(i+1,j1,k,l)j/n+dp(i,j+1,k1,l)k/n+dp(i,j,k+1,l1)l/n+i/n+j/n+k/n+l/n

d p ( i , j , k , l ) = d p ( i , j , k , l ) ∗ i / n + d p ( i + 1 , j − 1 , k , l ) ∗ j / n + d p ( i , j + 1 , k − 1 , l ) ∗ k / n + d p ( i , j , k + 1 , l − 1 ) ∗ l / n + 1 dp(i,j,k,l)=dp(i,j,k,l)*i/n+dp(i+1,j-1,k,l)*j/n+dp(i,j+1,k-1,l)*k/n+dp(i,j,k+1,l-1)*l/n + 1 dp(i,j,k,l)=dp(i,j,k,l)i/n+dp(i+1,j1,k,l)j/n+dp(i,j+1,k1,l)k/n+dp(i,j,k+1,l1)l/n+1
我们发现等式的左右两边都有dp(i,j,k,l)
那我们把右边的式子移到左边
d p ( i , j , k , l ) ∗ ( j + k + l ) / n = d p ( i + 1 , j − 1 , k , l ) ∗ j / n + d p ( i , j + 1 , k − 1 , l ) ∗ k / n + d p ( i , j , k + 1 , l − 1 ) ∗ l / n + 1 dp(i,j,k,l)*(j+k+l)/n=dp(i+1,j-1,k,l)*j/n+dp(i,j+1,k-1,l)*k/n+dp(i,j,k+1,l-1)*l/n + 1 dp(i,j,k,l)(j+k+l)/n=dp(i+1,j1,k,l)j/n+dp(i,j+1,k1,l)k/n+dp(i,j,k+1,l1)l/n+1
再将系数化为1
可得
d p ( i , j , k , l ) = d p ( i + 1 , j − 1 , k , l ) ∗ ( j + k + l ) + d p ( i , j + 1 , k − 1 , l ) ( j + k + l ) + d p ( i , j , k + 1 , l − 1 ) ∗ ( j + k + l ) + n / ( j + k + l ) dp(i,j,k,l)=dp(i+1,j-1,k,l)*(j+k+l)+dp(i,j+1,k-1,l)(j+k+l)+dp(i,j,k+1,l-1)*(j+k+l) + n/(j+k+l) dp(i,j,k,l)=dp(i+1,j1,k,l)(j+k+l)+dp(i,j+1,k1,l)(j+k+l)+dp(i,j,k+1,l1)(j+k+l)+n/(j+k+l)
我们惊奇的发现,左右两边都没有相等的项了
所以就不用一直递归下去了!
AC!

时间复杂度 O ( n 4 ) O(n^4) O(n4)
咋办?
不慌,我们发现,对于i,j,k,l而言,i+j+k+l=n
所以i=n-j-k-l
所以事实上,我们并不需要记录i,我们只需要记录j,k,l即可
在式子中去掉i,将系数中的i替换为n-j-k-l可得
d p ( j , k , l ) = d p ( j − 1 , k , l ) ∗ ( j + k + l ) + d p ( j + 1 , k − 1 , l ) ( j + k + l ) + d p ( j , k + 1 , l − 1 ) ∗ ( j + k + l ) + n / ( j + k + l ) dp(j,k,l)=dp(j-1,k,l)*(j+k+l)+dp(j+1,k-1,l)(j+k+l)+dp(j,k+1,l-1)*(j+k+l) + n/(j+k+l) dp(j,k,l)=dp(j1,k,l)(j+k+l)+dp(j+1,k1,l)(j+k+l)+dp(j,k+1,l1)(j+k+l)+n/(j+k+l)
这样就只需枚举j,k,l即可
时间复杂度 O ( n 3 ) O(n^3) O(n3)

#include<bits/stdc++.h>
using namespace std;
int n,in,a[5];
double dp[305][305][305];
signed main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&in);
        a[in]++;
    }
    for(int k=0;k<=n;k++)
    {
        for(int j=0;j<=n;j++)
        {
            for(int i=0;i<=n;i++)
            {
                if(i or j or k)
                {
                    if(i)
                        dp[i][j][k]+=dp[i-1][j][k]*i/(i+j+k);
                    if(j)
                        dp[i][j][k]+=dp[i+1][j-1][k]*j/(i+j+k);
                    if(k)
                        dp[i][j][k]+=dp[i][j+1][k-1]*k/(i+j+k);
                    dp[i][j][k]+=(double)n/(i+j+k);
                }
            }
        }
    }
    printf("%.12lf",dp[a[1]][a[2]][a[3]]);
}

总结

总结一下:
动态规划还可以细分为(1,最优解动态规划/2,计数型动态规划)

最优解动态规划

通用形式为 f ( i . . . . . ) = s o l v e ( f ( j . . . . . . ) , f ( k . . . . . . ) , . . . . . . ) f(i.....)=solve(f(j......),f(k......),......) f(i.....)=solve(f(j......),f(k......),......)
通用思想:对于一个的答案的最优解,可以分解为多个小答案的最优解取最优解,多个小答案再取最优解,直至不可取为止
对于这类的问题,必须满足以下条件:
1,最优解之间具有可比性,即对于两种不同答案的最优解,能够选出一个更优的解使得下一步决策时更优
2,数组能够容纳,即动态规划数组的参数必须是可以用一个唯一的整数表达出来的,且最大值小于等于 1 0 8 10^8 108
3,能够线性求出,即枚举过的数据不能再次枚举,否则即为深度优先搜索

计数型动态规划

通用形式为 f ( i . . . . . ) = ( f ( j . . . . . . ) ( + , − , ∗ , / ) f ( k . . . . . . ) , . . . . . . ) f(i.....)=(f(j......) (+,-,*,/) f(k......),......) f(i.....)=(f(j......)(+,,,/)f(k......),......)
通用思想:对于一个的答案的解,可以分解为多个小答案的解的并,多个小答案再取解的并,直至不可取为止
对于这类的问题,必须满足以下条件:
1,最优解之间具有可并性,即对于两种不同答案的解,他们之间能够结合起来而下一步处理时不影响总答案的数量
(例子:乘法原理,乘法分配律,加法结合律等)
2,数组能够容纳,即动态规划数组的参数必须是可以用一个唯一的整数表达出来的,且最大值小于等于 1 0 8 10^8 108
3,能够线性求出,即枚举过的数据不能再次枚举,否则即为深度优先搜索

做题技巧:

1,审题时,将所有会影响到答案的数据都纳入动规数组中,再考虑深度优化
2,有些最优解有时虽然不比另一个解更优,但是它有些条件与另一个解不同,可能产生更优的答案,也要考虑储存下来
3,对于一些一定不优的解,即可用最优解覆盖,不影响答案,计数型动规,即使是一些看起来较小的答案,只要有可能影响答案,也要记录
4,空间吃紧时,可以考虑滚动数组,覆盖一些后续不可能访问到的数组
5,优化时,可以从数组参数考虑优化,例如多个不同的参数表示出来的结果可以用一个参数表示出来,且这个参数的定义域更小时可以选择更改参数
6,优化时,也可以从枚举范围进行优化,如一个更接近的数组的答案包含一个更先前的答案时
7,一些题的答案并不在动态规划数组中,需要后续再结合动规数组得出答案
8,动态规划数组也可以视为预处理的一部分,后续再根据范围求解
9,一些动规的题目不只需要跑一遍动态规划,还可能根据实际变化的量枚举进行动规
10,一些题要求多个答案,且答案为多个动规数组的历史版本的并,空间又吃紧时,可以把问题离线下来,在动规的同时合并

至此,你已掌握基础动规的奥妙了
往后的学习中
你还会遇到诸如“树形dp”,“装压dp”,“插头dp”等等动态规划题目
但是他们的核心思想不变!
“分而治之”,这句话在动规中依旧适用!

课后小测NOIP2022原题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值