[笔记]动态规划入门

时间:2021-08-19
本文章向大家介绍[笔记]动态规划入门,主要包括[笔记]动态规划入门使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

以前存在本地的东西,发上来方便找(x)


动态规划

模型

背包

01背包\(f[i][j]=\max(f[i-1][j],f[i-1][j-w_i]+v_i)\)

完全背包(每个物品次数不限),那就用\(f[i][j]=\max(f[i-1][j],f[i][j-w_i]+v_i)\),滚动数组的时候对于体积直接从小到大枚举就好。

多重背包(第\(i\)种物品有\(k_i\)个),大暴力是\(O(nm\sum k_i)\),二进制拆分\(k_i=1+2+\dots+2^t+(k_i-(1+2+\dots,2^t))\),复杂度\(O(nm\sum\log k_i)\)

int x;scanf("%d",&x);
int c=1;
while(x>c){
    x-=c;
    w[++n]=W[i]*c;
    v[n]=V[i]*c
    c<<=1;
}
if(x)w[++n]=W[i]*x,v[n]=V[i]*x;

单调队列优化多重背包

暴力dp:\(f[i][j]=\max_{k=0}^{k_i}(f[i-1][j-k*w[i]]+k*v[i])\),这样对每一个\(i\)转移的代价是\(O(m\sum k_i)\)的。接着我们注意到,对于\(0\leq j<w[i]\)\(f[i-1][j],f[i-1][j+w[i]],f[i-1][j+2w[i]],\dots\)这样一系列位置会对\(f[i][j+kw[i]]\)造成影响,我们考虑枚举\(j=0,\dots,w[i]-1\),再去枚举\(k\),用单调队列来优化求\(f[i]\)的过程,这样总复杂度就是\(O(n\sum w_i\frac{W}{w_i})=O(nW)\),其中\(W\)是背包的大小。

二维费用背包:和普通背包类似,多记录一维代价。

分组背包:组内的物品互相冲突不能同时选择,改成先枚举组,然后是体积,最后才是组内的所有物品:

for(int k=1;k<=K;k++)for(int j=V;j>=0;j--)for(int i=1;i<=size[k];i++)...

最外面的一个k类似于普通背包的第几个物品,最里面一层枚举组内的物品,就保证了每组最多选一个(因为是从先从大到小枚举体积\(j\),那么对于\(f[j],f[j-c_i]\)这些只能由上一组的状态转移过来)

泛化物品:每个物品可能没有固定的价值/体积,价值\(V\)是关于体积的函数\(f(W)\)

数位DP

ll dfs(int x,int pre,int st,……,int lead,int limit){
	if(!x) return st;
	if((dp[x][pre][st]…[]!=-1&&!limit&&!lead))return dp[x][pre][st]…[];
	ll ret=0;
	int mx=limit?a[x]:9;
	rep(i,0,mx){
        //有前导0并且当前位也是前导0
		if(!i&&lead) ret+=dfs(……,……,……,i==mx&&limit);
        //有前导0但当前位不是前导0,当前位就是最高位
		else if(i&&lead) ret+=dfs(……,……,……,i==mx&&limit); 
		else if(根据题意而定的判断) ret+=dfs(……,……,……,i==mx&&limit);
	}
	if(!limit&&!lead) dp[x][pre][st]……[……]=ret;
	return ret;
}
ll solve(ll x){
	len=0;
	while(x) a[++len]=x%10,x/=10;
	memset(dp,-1,sizeof dp);//初始化-1(因为有可能某些情况下的方案数是0)
	return dfs(……,……,……,……);//进入记搜
}
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%lld%lld",&l,&r);
	    if(l) printf("%lld",solve(r)-solve(l-1));//[l,r](l!=0)
	    else printf("%lld",solve(r)-solve(l));//从0开始要特判,按情况看看要不要加别的
	}
	return 0;
}

线性

http://poj.org/problem?id=2279

$ N$个学生合影,站成左端对齐的 \(k\)排,每排分别有\(N1,N2,\dots,N_k\)个人。 \((N1≥N2≥…≥Nk)\)

第1排站在最后边,第 \(k\) 排站在最前边。 学生的身高互不相同,把他们从高到底依次标记为\(1,2,…,N\)

在合影时要求每一排从左到右身高递减,每一列从后到前身高也递减。问一共有多少种安排合影位置的方案?(\(k\leq 5,\sum N_i\leq 30\))

线性dp容易给出一个,以及注意一种\(O(30^5)\)的做法,但是这题会炸空间。

这个背景是和整数拆分有关的【杨氏矩阵】,记\(n=\sum N_i\),每个位置\((i,j)\)所有它右边和下方(如果有)的格子个数(包括自己)称为这个“钩子”的长度,这题对应的答案就是\(n!/\Pi\)钩子长度,证明不会。

https://www.acwing.com/problem/content/274/

最长上升公共子序列,LIS是\(f[i]\)记录以\(i\)为结尾的LIS,LCS则是记录\(1-i\)\(1-j\)的,这里就考虑综合起来,\(f[i][j]\)表示以\(s[1,\dots,i]\),以\(t[j]\)为结尾的LCIS的长度,类似LCS,如果\(a[i]\neq b[j]\),直接\(f[i][j]=f[i-1][j]\),否则如果\(a[i]=b[j]\),就有转移\(\begin{aligned}f[i][j]=\max_{k=1,\dots,j-1,b[k]<b[j]}(f[i-1][k])+1\end{aligned}\),又\(b[j]=a[i]\),下面改成\(b[k]<a[i]\),原本\(O(n)\)的转移就可以优化了:枚举\(i\)之后对固定的\(i\),枚举\(j\)的同时更新这个\(max\),整体就能做到\(O(n^2)\)

https://www.acwing.com/problem/content/275/

\(a[]\),需要构造一个单调不减或者单调不增的\(b[]\),最小化\(\sum |a_i-b_i|,n\leq 2000\)

容易想到先考虑单调不增或者单调不减的其中一种情况,另一种把\(a[]\)翻转过来再做一遍就行。就以考虑单调不减的序列为例,用\(f[i][j]\)表示前\(i\)个,以\(j\)作为\(b[]\)的结尾的最小答案,这样就有\(\begin{aligned}f[i][j]=\min_{k=1,\dots,j}f[i-1][j]+|a_i-j|\end{aligned}\),不过这样值域有点大,注意到任何一个\(b[j]\)的取值,一定是取\(a[]\)中的某个项,不然就浪费了,所以可以离散化一下,复杂度\(O(n^3)\),进一步,这题其实和上题类似,对于一个固定的\(i\),决策集合只增不减,\(O(n)\)的转移可以和枚举\(j\)一起进行,时间复杂度\(O(n^2)\)

关于\(b[j]\)一定取\(a[]\)中的某个值可以考虑一个数学归纳:假设对\(1,\dots,k-1\)成立,对于\(i=k\)来说,如果\(b[k-1]\leq a[k]\),直接构造\(b[k]=a[k]\)就做到了最优的;否则考虑让\(b[k]=b[k-1]\),根据归纳我们能够说明\(b[k-1]\)取的是\(a[1,\dots,k-1]\),但这还不够,我们需要证明我们取\(b[k]=b[k-1]\)这种决策能够达到最优,想一下也显然,考虑从\(i=k\)往前的一段区间\([j,k]\),这一段的\(b[i]\)都取得相同的\(v\),那这就变成给一个\(a[]\),最小化\(\sum |a_i-v|\)的问题了,这个问题很经典,显然最优情况下的\(v\)能够取得到\(a[]\)中的值。

https://www.acwing.com/problem/content/276/

\(L\)个点的有向往全图,每条边有边权(即对应\(u\to v\)的代价),有3个人一开始分别在1,2,3处,\(1-n\)个时刻,每个时刻需要有一个人在\(p_i\)这个点,同一个时刻只有同一个时刻一个点不能有多个人,问满足所有需求需要的最小代价。\(L\leq 200,n\leq 1000\)

考虑DP还是先想怎么记录状态,依然先考虑最暴力的,三个人的位置肯定要能够被记录进去,同时还要记录处理到了哪个时刻,于是就有了\(O(L^3n)\)级别的状态,太大了没法跑。

很套路地想怎么去掉一些状态,任意一个时刻一定会有一个人处于\(p_i\)的位置,于是就可以去掉一个\(L\),比如\(f[i][a][b]\)表示到时刻\(i\),一个人在\(p[i]\),另外两个人分别在\(a,b\)这两个位置,转移也很好想,即\(p_i/a/b\to p_{i+1}\)三种转移方式,实现的时候判定一下状态合法即可。

背包

https://codeforces.com/problemset/problem/19/B

Bob拿着\(n\)件商品在收银台付款,扫描第\(i\)件商品需要\(t_i\)的时间,第\(i\)件的价格为\(c_i\),在扫描的时候可以选择偷走一些商品,偷走一个商品需要1个单位的时间,问最少花多少钱能获得所有商品。\(n\leq 2000,t_i\leq 2000\)

相当于把商品划分成两个集合\(S,T\),满足\(|T|\leq \sum_{S}t_i\),使得\(\sum_{S}c_i\)最小,左边的式子稍微变形会得到\(\sum_{T}t_i+|T|=\sum_{T}(t_i+1)\leq \sum t_i\),这就很像背包了:反着考虑获得哪些物品不要花钱,总容量为\(\sum t_i\),选择一件物品不花钱得到的代价是\(t_i+1\),这样一个01背包问题,但是复杂度会达到\(O(nt^2)=O(n^3)\),接受不了。

不过顺着这个背包的思路继续想,选择花钱买一件物品相当于多获得一个\(t_i+1\)的体积,对应的付出\(c_i\)的代价,最终目标是获得所有物品,即\(\sum_{S}t_i+1\geq n\),于是又是一个背包:选择花钱购买一些物品,使得这些物品的体积之和超过\(n\),求最小的代价。同样的问题又来了,这样做背包的体积上界是多少?如果还是\(O(n^2)\)级别的话这个优化就没什么用了:仔细想一下上界不会很大,在一系列决策之后如果当前的\(\sum t_i+\)已经超过了\(n\),那后面的一定不会继续选择购买,所以最大的情况一定是从一个小于\(n\)的体积跨越到一个大于\(n\)的体积,对应的上界就是\(n-1+(v_{max}+1)=n+v_{max}\)了。

https://www.luogu.com.cn/problem/P4141

背包问题变形,\(n\)个物品,需要回答如果没有第\(i\)个物品的时候,恰好装满容量为\(x=1,\dots m\)的背包需要多少代价?\(n,m\leq 2000\)

原问题是\(dp[i][j]=dp[i-1][j]+dp[i-1][j-v[i]]\)\(dp[0][0]=1\),暴力做是直接\(O(n^2m)\)的,先处理处没有第一个物品的答案,然后考虑如何给dp删除一个物品和添加物品。

按照滚动数组得到的dp数组考虑,注意到物品顺序不影响答案,假设当前得到的是\(f[0,\dots,m]\),删去物品\(i\)之后的答案是\(g[1,\dots,m]\),则有当\(j<v[i]\)时,\(f[j]=g[j]\),当\(j\geq v[i]\)\(f[j]=g[j]+g[j-v[i]]\),于是就能从小到大反推出\(g[]\),这样每一次只需要\(O(m)\)的代价计算删除和加入一个物品的答案。

rep(i,2,n){ 
    rep(j,0,w[i]-1)g[j]=f[j];
    rep(j,w[i],m)g[j]=(f[j]-g[j-w[i]]+10)%10;

    rep(j,0,m)f[j]=g[j];
    for(int j=m;j>=w[i-1];j--)upd(f[j],f[j-w[i-1]]);
    rep(j,1,m)printf("%c",f[j]+'0');
    puts("");
}

https://www.luogu.com.cn/problem/P1877

01背包变形,\(dp[i][j]\)\(dp[i-1][j-c[i]]\)\(dp[i-1][j+c[i]]\)两个转移。

https://www.luogu.com.cn/problem/P1509

二维背包变形,两个代价(rmb和rp)以及两个收益(在泡到最多MM的前提下时间最小),可以考虑两个dp数组,在MM最多的前提下再比较第二维,或者像我在实现的时候直接开个struct来存dp,重载一个比较函数。

https://www.luogu.com.cn/problem/P3985

二维背包变形,一开始理解错题意,以为是DP的时候多带一个极差\(\leq3\)的限制,后面发现原来是给的数据保证极差\(\leq 3\),那就好做了,考虑最后选择的物品的集合是\(S\),最后要\(\sum_{i\in S} v_i\leq W\),取\(X=\min_i(v_i)\),式子变成\(X|S|+\sum_{i\in S}(v_i-X)\leq W\),考虑个双重限制的背包:一个是\(v_i\in{0,1,2,3}\),和第二维代价:每选一个物品代价是1,先对这个二维背包进行DP,然后再统计符合条件的答案。

https://www.luogu.com.cn/problem/P1455

并查集维护连通块,然后直接对每一个联通块01背包

https://www.luogu.com.cn/problem/P1858

\(K\)个人,每个人有一个背包,容量都是\(V\)\(N\)件物品,现在要每个人都能恰好装满背包,并且任意两个人选的物品不完全相同,所有人价值之和的最大\(K\leq 50,V\leq 5000,N\leq 200\)

发现相当于在求一个01背包要求完全装满的前\(K\)大值,前\(K\)大的处理一般是把普通的DP式子转化成一个单调队列来维护,转移变成\(O(k)\)地归并状态。

rep(i,1,n)per(j,V,v[i]){
	tmp.clear();
	for(auto itr:f[j])tmp.pb(itr);
	f[j].clear();

    int p=0,q=0;
    while((p<tmp.size()||q<f[j-v[i]].size())&&(p+q<=k)){
        int sp=tmp.size(),sq=f[j-v[i]].size();
        if(q>=sq||(p<sp&&q<sq&&tmp[p]>w[i]+f[j-v[i]][q]))f[j].pb(tmp[p++]);
        else f[j].pb(w[i]+f[j-v[i]][q++]);
    }
}

https://www.luogu.com.cn/problem/P1776

完全背包问题,带log的和单调队列优化的都能过,暴力的还没试过(

https://www.luogu.com.cn/problem/P5322

(一道评价还不错的dp题)手上的士兵很像背包的容量,而对于每座城堡来说又可以看成一个类似于“物品”的东西,就转换成了一个类似01背包的问题,但是因为有多个玩家,我们对每座城堡所有玩家的\(a_i\)升序排列,然后在从大到小枚举体积\(j\)的时候直接f[j]=max(f[j],f[j-2*a[i]-1])+i 就行(因为排序之后就保证了不会在同一个\(i\)内部重复计算。

https://www.luogu.com.cn/problem/P1782

多重背包+泛化背包,不过这个泛化背包的处理也很暴力…以及这题有点卡常

单调栈/单调队列

https://www.luogu.com.cn/problem/P2254

给你一张地图,一些地方不能走,输入初始位置,\(K\)段时间,每段时间内要么只能往指定的方向走,要么不走,问最远能走多长的路径。\(n,m,k\leq 200\)

\(f[i][j][k]\)表示第\(k\)段时间走完之后在\((i,j)\)处的答案,暴力转移\(\begin{aligned}f[i][j][k]=\max_{t=0}^{ed_k-st_k+1}\{f[i-t\Delta x][j-t\Delta y][k-1]+t\}\end{aligned}\),这样就得到了一个四次方的DP,但是应该是数据太水(以及可能因为是十几年前的题了,当时测评机没这么快)…这么个大暴力就过了这题:

rep(k,1,K)rep(i,1,n)rep(j,1,m)if(avl[i][j]){
    int mx=-INF;
    rep(t,0,ed[k]-st[k]+1){
        int cx=i-t*di[d[k]],cy=j-t*dj[d[k]];
        if(!check(cx,cy))break;
        if(f[cx][cy][k-1]==-INF)continue;
        mx=max(mx,f[cx][cy][k-1]+t);
    }
    f[i][j][k]=mx;
}

不过还是来优化转移过程,其实\(\Delta x,\Delta y\)里面会有一个是0,假设\(j\)定下来,\(i\)是这次动的方向,我们对于每个确定的\(k,j\),其实可以\(O(n)\)

\(f[i][j][k]=\max_{t=0}^{len}\{f[i-t\Delta x][j][k-1]+t\}\)进行转移:假设这个\(d[k]\)意味着\(i\)只能增大,那我就从1枚举\(i\),用单调队列维护\(f[i-t\Delta x][j][k-1]-t\),每次再用\(Q.front()+t\)来更新答案(注意前面单调队列里维护的是\(-t\),因为原来dp式子里的\(t\)的含义其实是两个位置的距离,相当于是\(t_{当前}-t_{从哪转移来}\)

数位

http://acm.hdu.edu.cn/showproblem.php?pid=2089

数位上不能有连续的62,以及不能有4。

https://www.luogu.com.cn/problem/P2602

统计\([l,r]\)内所有数码出现的次数,一位一位数码考虑,要算数位出现的次数,答案也丢到dfs里面来跑,状态多记几个不会爆的…

ll dfs(int x,int cnt,int D,bool limit,bool lead)
{
	if(~f[x][cnt][limit][lead])return f[x][cnt][limit][lead];
	if(!x)return f[x][cnt][limit][lead]=cnt;
	int mx=limit?digit[x]:9;
	ll ret=0;
	rep(i,0,mx)
	{
		int cnt2=cnt;
		if(i)cnt2+=(i==D);
		else cnt2+=(!lead&&i==D);
		ret+=dfs(x-1,cnt2,D,limit&&i==digit[x],lead&&!i);
	}
	return f[x][cnt][limit][lead]=ret;
}

https://codeforces.com/problemset/problem/276/D

考虑\(a,b\in [l,r]\),问\(a\oplus b\)的最大值,\(l,r\leq 10^{18}\)

异或想到考虑二进制,但是不太会考虑,那就大力枚举,数位dp不就是干这种事情嘛?

\(f[x][l1][r1][l2][r2]\)表示从高往低考虑到第\(x\)位,\(l1,r1\)表示对\(a\)的约束,同样\(l2,r2\)是对\(b\)的约束。

if(!x)return 0;
if(~f[x][l1][r1][l2][r2])return f[x][l1][r1][l2][r2];
ll ans=0;
//l1,r1 means first number's upper and lower bound 
rep(i,l1?L[x]:0,r1?R[x]:1)
    rep(j,l2?L[x]:0,r2?R[x]:1){
        ll t=i^j;t<<=(x-1);
        t+=dfs(x-1,l1&&i==L[x],r1&&i==R[x],l2&&j==L[x],r2&&j==R[x]);
        ans=max(ans,t);
    }
return f[x][l1][r1][l2][r2]=ans;

树形

https://www.luogu.com.cn/problem/P1352

入门题

http://acm.hdu.edu.cn/showproblem.php?pid=2196

对树上每个点求它到哪个点距离最远,一个\(O(n\log n)\)的做法是先\(O(n)\)求出直径\(r_1,r_2\),那么每个点\(x\)的答案就是\(max\{dis(r_1.x).dis(r_2.x)\}\),注意这题有多组数据(以及一开始以为没清空干净数组,疯狂MLE/TLE)。

原文地址:https://www.cnblogs.com/yoshinow2001/p/15163111.html