动态规划:DP从入门到破门而出(入门必刷例题)

有时间就会更新QwQ

例题难度随更新时间可能增大

例题顺序按照更新时间排序的哦QuQ

特别鸣谢 长江东逝水 对本Blog的建议与指正!

目录:

1.区间DP
    例题1:P2858 奶牛零食
    例题2:P3146 [USACO16OPEN]248 / P3147 [USACO16OPEN]262144
    例题3:P3205 [HNOI2010]合唱队
    例题4:P4170 [CQOI2007]涂色
    例题5:P1880 [NOI1995]石子合并
    
2.前缀DP
    例题1:P3628 【APIO2010】特别行动队( O(n ^ 2) )
    例题2:P1944 最长括号匹配
    例题3:P1020 导弹拦截(最长上升子序列)

3.背包DP
    例题1:P1048 采药(01背包)
    例题2:P1616 疯狂的采药(完全背包)
    例题3:P1064 金明的预算方案(带附件的01背包)
    例题4:P5020 货币系统

4.树形DP
    例题1:P2015 二叉苹果树
    例题2:P1352 没有上司的舞会
    例题3:P2016 战略游戏
    例题4:UVA1218 完美的服务 Perfect Service

5.状压DP
    例题1:P1896 [SCOI2005]互不侵犯
    例题2:P1879 [USACO06NOV]玉米田Corn Fields
    例题3:P2704 [NOI2001]炮兵阵地

6.斜率优化DP
    例题1:P3628 【APIO2010】特别行动队( O(n) )

1.区间DP

例题1:P2858 奶牛零食

约翰经常给产奶量高的奶牛发特殊津贴,于是很快奶牛们拥有了大笔不知该怎么花的钱.为此,约翰购置了N(1≤N≤2000)份美味的零食来卖给奶牛们.每天约翰售出一份零食.当然约翰希望这些零食全部售出后能得到最大的收益.这些零食有以下这些有趣的特性:

•零食按照1..N编号,它们被排成一列放在一个很长的盒子里.盒子的两端都有开口,约翰每

天可以从盒子的任一端取出最外面的一个.

•与美酒与好吃的奶酪相似,这些零食储存得越久就越好吃.当然,这样约翰就可以把它们卖出更高的价钱.

•每份零食的初始价值不一定相同.约翰进货时,第i份零食的初始价值为Vi(1≤Vi≤1000).

•第i份零食如果在被买进后的第a天出售,则它的售价是vi×a.

Vi的是从盒子顶端往下的第i份零食的初始价值.约翰告诉了你所有零食的初始价值,并希望你能帮他计算一下,在这些零食全被卖出后,他最多能得到多少钱.

思路:

首先想到这是一道区间DP,我们设dp[i][j]为从第i个到第j个零食最多可以买多少钱。不难想到我们可以在一个状态下左面或右面补充,则可得:

dp[i][j] = max(dp[i + 1][j] + b[i] * a, dp[i][j - 1] + b[j] * a)

其中a是指题目中所说的,我们可以在枚举区间长度时算出来:

a = n - L + 1

不过要注意,初始化时,每一个物品最后买出去的时候都是这个物品的价格 * n(根据题意)

例题2:P3146 [USACO16OPEN]248 / P3147 [USACO16OPEN]262144

Bessie喜欢在手机上下游戏玩(……),然而她蹄子太大,很难在小小的手机屏幕上面操作。

她被她最近玩的一款游戏迷住了,游戏一开始有n个正整数,(2<=n<=248 / 262144),范围在1-40。在一步中,贝西可以选相邻的两个相同的数,然后合并成一个比原来的大一的数(例如两个7合并成一个8),目标是使得最大的数最大,请帮助Bessie来求最大值。

这两道题是几乎一样的题,但是数据范围不一样。

思路1:

我们设计dp[i][j]为从i到j可以合成的最大的数。

考虑小区间合成大区间,dp[i][k] && dp[k + 1][j] -> dp[i][j]

如果这两个区间可以合成的最大值相等,那么说明我们可以把这两个小区间合成一个大区间,即:

if(dp[i][k] == dp[k + 1][j])
    dp[i][j] = max(dp[i][j], dp[i][k] + 1);

如果不能合成,就直接不修改。

时间复杂度O(n^3)

思路2:

貌似O(n^3)不能过第二道题……

我们考虑前缀DP的想法。我们设计dp[i][j]以j为左端点合成i的右端点位置

我们要注意这里i最大就是60,j最大就是n,所有空间没有问题。

我们要求dp[i][j]时,我们可以先求dp[i - 1][j],得到取i - 1的右端点,然后再取一个右端点,这两个区间合并就是i了,即:

dp[i][j] = dp[i - 1][dp[i - 1][j]];

我们只需要枚举i和j就好了,时间复杂度O(60n)。

例题3:P3205 [HNOI2010]合唱队

题目描述
为了在即将到来的晚会上有更好的演出效果,作为AAA合唱队负责人的小A需要将合唱队的人根据他们的身高排出一个队形。假定合唱队一共N个人,第i个人的身高为Hi米(1000<=Hi<=2000),并已知任何两个人的身高都不同。假定最终排出的队形是A 个人站成一排,为了简化问题,小A想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终棑排出的队形中:

-第一个人直接插入空的当前队形中。

-对从第二个人开始的每个人,如果他比前面那个人高(H较大),那么将他插入当前队形的最右边。如果他比前面那个人矮(H较小),那么将他插入当前队形的最左边。

当N个人全部插入当前队形后便获得最终排出的队形。

例如,有6个人站成一个初始队形,身高依次为1850、1900、1700、1650、1800和1750,

那么小A会按以下步骤获得最终排出的队形:

1850

1850 , 1900 因为 1900 > 1850

1700, 1850, 1900 因为 1700 < 1900

1650 . 1700, 1850, 1900 因为 1650 < 1700

1650 , 1700, 1850, 1900, 1800 因为 1800 > 1650

1750, 1650, 1700,1850, 1900, 1800 因为 1750 < 1800

因此,最终排出的队形是 1750,1650,1700,1850, 1900,1800

小A心中有一个理想队形,他想知道多少种初始队形可以获得理想的队形

数据范围:n <= 1000,主要取模19650827(不是那个十分常见的大质数

思路:

类似奶牛奶酪零食的思路差不多,都是两边补数的思想

我们设计dp1[i][j]为形成区间[i, j],且最后一个进入这个区间的数是i的方案数,dp2[i][j]为形成区间[i, j],且最后一个进入这个区间的数是j的方案数。

对于dp1[i][j],我们发现:

作为左端点的数只与下一个和右端点有关,即:

如果a[i] < a[i + 1],则

dp1[i][j] += dp2[i + 1][j];

如果a[i] < a[j],则

dp1[i][j] += dp1[i + 1][j];

相同的,对于dp2[i][j],我们一样可以得知:

作为右端点的数只与左端点和前一个有关,即:

如果a[j] > a[j - 1],则

dp2[i][j] += dp2[i][j - 1];

如果a[j] > a[i],则

dp2[i][j] += dp1[i][j - 1];

例题4:P4170 [CQOI2007]涂色

假设你有一条长度为5的木版,初始时没有涂过任何颜色。你希望把它的5个单位长度分别涂上红、绿、蓝、绿、红色,用一个长度为5的字符串表示这个目标:RGBGR。

每次你可以把一段连续的木版涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。例如第一次把木版涂成RRRRR,第二次涂成RGGGR,第三次涂成RGBGR,达到目标。

用尽量少的涂色次数达到目标。

数据范围:100%的数据满足:1<=n<=50

思路:

我们可以考虑设计dp[i][j]区间[i, j]需要的最少操作次数,我们可以通过枚举断点的方式考虑。

我们只需向奶牛零食那样,补左端点右端点就好了。

对于dp[i][j]:

如果ch[i] == ch[j],说明我们可以直接先修改i ~ j,所以我们直接合并i ~ j - 1与j或是i 与 i + 1 ~ j,即

if(ch[i] == ch[j])
    dp[i][j] = min(dp[i][j - 1], dp[i + 1][j]);

如果ch[i] != ch[j],说明我们不能直接更新,我们再枚举i ~ j的一个k,寻找一个合并答案的最小值,即:

if(ch[i] != ch[j])
    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);

时间复杂度O(n^3),稳过50

例题5:P1880 [NOI1995]石子合并

在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分.

主要思路:

我们设计dp_min[i][j]为区间[i, j]的最小得分,dp_max[i][j]为区间[i, j]的最大得分。

我们可以先维护一个前缀和,便于直接求区间和。

然后,对于dp_min[i][j],每一个k∈(i, j],我们可以从k处端点,然后合并dp_min[i][k]和dp_min[k + 1][j],即

dp_min[i][j] = min(dp_min[i][j], dp_min[i][k] + dp_min[k + 1][j] + sum[j] - sum[i - 1]);

对于dp_max[i][j]也一样,即

dp_max[i][j] = max(dp_max[i][j], dp_max[i][k] + dp_max[k + 1][j] + sum[j] - sum[i - 1]);

最后寻找最大值即可。

(尽管都说这是区间DP的入门题,但我觉得入门时做这道题不算是最简单的)

时间复杂度:O(n^3)

2.前缀DP

例题1:P3628【APIO2010】特别行动队

给一个长度为\(n\)的序列A[],分成若干段,每分成一段,其代价是\(ax^2 + bx + c\)\(x\)为这一段的和,问分割最大的代价是多少?

(暂不考虑优化)要求:\(O(n^2)\)

思路:

\(dp[i]\)为分到第\(i\)个数时,其最大代价是多少。

我们可以枚举上一段分开的位置,即分\([1, i]\)\([1, k],[k + 1, i]\)两段。

其整段的和可用前缀和维护。

转移方程:

dp[i] = max{dp[j] + a*(s[i]-s[j])*(s[i]-s[j]) + b*(s[i]-s[j]) + c}

例题2:P1944 最长括号匹配

对一个由(,),[,]括号组成的字符串,求出其中最长的括号匹配子串。具体来说,满足如下条件的字符串成为括号匹配的字符串:

1.(),[]是括号匹配的字符串。

2.若A是括号匹配的串,则(A),[A]是括号匹配的字符串。

3.若A,B是括号匹配的字符串,则AB也是括号匹配的字符串。

例如:(),[],([]),()()都是括号匹配的字符串,而](则不是。

字符串A的子串是指由A中连续若干个字符组成的字符串。

例如,A,B,C,ABC,CAB,ABCABCd都是ABCABC的子串。空串是任何字符串的子串。

数据范围:对100%的数据,字符串长度<=1000000.

思路:

我们可以设计dp[i]为到i时的最长括号匹配长度。

那我们可以得到,ch[i - dp[i]]就是以i为最右端点的最长括号匹配的左端点,我们只需要判断以i - 1为右端点的最长括号匹配的左端点的前一个字符是不是和第i个括号匹配就是了,如果是,就更新答案。即:

if((ch[i - 1 - dp[i - 1]] == '(' && ch[i] == ')') || (ch[i - 1 - dp[i - 1]] == '[' && ch[i] == ']')) {
    dp[i] = dp[i - 1] + 2 + dp[i - 2 - dp[i - 1]];

如果是左括号的话,就直接continue掉好啦。

时间复杂度O(n)

例题3:P1020 导弹拦截

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是 \le 50000≤50000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

数据范围:

子问题1:n <= 1000

子问题2:n <= 100000

思路:

首先第一个问题,其实就是求最长不上升序列。

我们设计 dp[i] 为从 1 到 i 且其最长不上升序列以 i 为结尾的序列长度。那么我们可以从比当颗导弹高或相等的导弹中更新答案。即为:

dp[i] = max{dp[v]} + 1 (a[v] >= a[i] && v <= i)

这里时间复杂度为O(n^2)。

对于第二个问题,我们可以想到,假如我们先用一个拦截设施把所有能打下来的导弹都打下来,剩下的拦截设施重复这个动作,一直到所有导弹被拦截。假设我们得到的是最小划分(题目要求)的k组导弹。那么对于第 i 组导弹中任取,在第 i + 1 必定有一个导弹要比这颗导弹高,如果没有,那么我们完全可以把 i + 1 组合并到第 i 组。所以我们找到一个最长的一个上升子序列,肯定序列中的每个导弹对应组导弹,如果多出的话肯定是多余的,如果少的话,不符合上述的分组。所以我们在找最小划分时,只需要找到一个最长上升子序列长度即可。

我们重新设计 dp[i] 为从 1 到 i 且其最长上升子序列以 i 为结尾的序列长度。同上,我们可以得到:

dp[i] = max{dp[v]} + 1 (a[v] < a[i] && v <= i)

时间复杂度也为O(n^2)

对于优化:(两问通用)

我们可以发现有一维是用来求 dp[] 中满足条件的最大值。我们可以考虑用线段树来优化。我们以 a[i] (就是高度)为坐标,在线段树中维护 f[i] 的最大值。我们在进行有规律的遍历时,每当更新一次 f[i] 我们就将他在线段树中更新,在寻找最大值时,我们按照大小,在相应区间(比如求最长上升子序列中,我们要从高度小于 a[i] 的 f[v] 中转移,所以我们询问 [1, a[i] - 1] 区间的信息)询问,并进行转移即可。

时间复杂度为O(nlogn)

3.背包DP

例题1:P1048 采药(01背包)

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

数据范围:M <= 100

思路:

其实数据范围还是很水的了。这是一道简单的01背包问题。

我们设计dp[i][j]为选到第i个时,容量用了j以后所得到的最大值。我们设v[i]为第i个物品的价值,w[i]为第i个物品的重量/体积。

对于dp[i][j],我们可以由选到第i - 1个,用了j - w[i]的容量的最大值来更新(也就是说为新填进去的物品腾空)我们不难推出:

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

这里注意一下边界,在枚举容量时,是从w[i] ~ m的,也就是说本来就没用w[i],不可能多腾出w[i]的空间。

扩展:

要求空间限制O(n)?

我们可以尝试着滚动一维。我们可以发现i这一维只有i - 1和i,我们可以考虑把这一维滚动掉(因为之前的状态只会用于下一层循环),如:

dp[j] = max(dp[j], dp[j - w[i]] + v[i]);

但是,我们想到,如果是正向循环的话,会使得同一层循环后面的操作直接使用当层得出的答案,也就是变成了从dp[i][j - w[i]]到dp[i][j]了,这样显然是不对的。

为了避免这一问题,我们可以把第二层循环反向一下,即:

for(int i = 1; i <= n; ++i)
    for(int j = m; j >= w[i]; --j)
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);

其实正向循环也有其意义,下一题会讲。

例题2:P1616 疯狂的采药(完全背包)

LiYuxiang是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是LiYuxiang,你能完成这个任务吗?

每种草药可以无限制地疯狂采摘。

思路:

和上一个题差不多啊,就是每种物品有无限个。这里我们从上一题所说的讲起。我们想一想,如果例题1中的优化解法用正向的话,我们可以把使得dp[i][j - w[i]]转移到dp[i][j],也就是可以重复选很多次了。这样,我们就可以使得重复这个问题得以解决了,即:

for(int i = 1; i <= n; ++i)
    for(int j = w[i]; j <= m; ++j) // 变化的位置
        dp[j] = max(dp[j], dp[j - w[i]] + v[i]);

例题3:P1064 金明的预算方案(带附件的01背包)

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件 附件

电脑 打印机,扫描仪

书柜 图书

书桌 台灯,文具

工作椅 无

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有0个、1个或2个附件。附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为55等:用整数1-5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是10元的整数倍)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第jj件物品的价格为v_[j],重要度为w_[j],共选中了kk件物品,编号依次为j_1,j_2,…,j_k,则所求的总和为:

v[j1]×w[j1]+v[j2]×w[j2]+…+v[jk]×w[jk]。

请你帮助金明设计一个满足要求的购物单。

思路1:

我们可以分类讨论,分主附件各种讨论,剩下的和普通01背包DP没啥区别。这里不详讲了,因为思路太乱了。

思路2:

我们可以考虑把这些物品连成树。一个节点的父亲就是这个物品所依赖的物品,儿子节点就是依赖这个物品的物品。

这样我们就可以进行树形DP了,说白了就是在dfs的每个状态下跑一边01背包,就是说这里的物品不是枚举每个物品,而是枚举子节点的物品罢了。
在每个dfs最后都要把当前修改过的dp值都要加上当前的价值。

dfs的代码:

inline void dfs(int x) {
    go(i, 0, s[x], 1) f[x][i] = 0;
    rep(i, x) {
        int v = e[i].v;
        dfs(v);
        fo(j, n, s[v], 1)
            fo(k, j, s[v], 1)
                f[x][j] = max(f[x][j], f[x][j - k] + f[v][k]);
    }
    if(x != 0)
        fo(i, n, s[x], 1)
            f[x][i] = f[x][i - s[x]] + s[x] * z[x];
}

例题4:P5020 货币系统

在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。为了方便,我们把货币种数为 n、面额数组为a[1..n] 的货币系统记作(n,a)。

在一个完善的货币系统中,每一个非负整数的金额 x 都应该可以被表示出,即对每一个非负整数 xx,都存在 n 个非负整数 t[i] 满足a[i]×t[i] 的和为 x。然而, 在网友的国度中,货币系统可能是不完善的,即可能存在金额 xx 不能被该货币系统表示出。例如在货币系统 n=3, a=[2,5,9] 中,金额 1,3 就无法被表示出来。

两个货币系统 (n,a) 和 (m,b) 是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。

现在网友们打算简化一下货币系统。他们希望找到一个货币系统 (m,b),满足 (m,b) 与原来的货币系统 (n,a) 等价,且 m 尽可能的小。他们希望你来协助完成这个艰巨的任务:找到最小的 m。

思路1:完全背包

也是一个所有的可以排出来的都枚举出来罢了,我们做n次完全背包,然后把可以得到的最大答案的数消去就好啦。(如果这几个数中有一个或几个可以用其他的数字得到,我们就可以消去这个数,让总数-1)

要记得排序,要不可能会出现一些问题。

思路2:判重

我们可以单用完全背包的思想来做就好。我们做模拟就好了。开一个较大的数组,相应的位置i表示是否可以表示i。即(也需要排序):

m = 2500 + a[n]; // 最大的判重范围
ff[0] = 1;
go(i, 0, m, 1) {
    if(ff[i]) {
        go(j, 1, n, 1) {
            ff[i + a[j]] = 1;
            if(f[i + a[j]] && i + a[j] != a[j]) {
                f[i + a[j]] = 0;
                ans--;
            } 
        }
    }
}

4.树形DP

例题1:P2015 二叉苹果树

有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)

这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。

我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树

2   5
 \ / 
  3   4
   \ /
    1

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。

给定需要保留的树枝数量,求出最多能留住多少苹果。

数据范围:n <= 100

思路:

首先设计f[i][j]为以i为根的子树中保留j个边的最大权值和。

我们枚举j是这个子树中所保留的所有的边数。枚举一个k,代表有k条边是其中一棵子树保留的边数,那么j - k - 1就是剩下保留的边数。

所以我们在树上做DFS,从叶节点到根节点进行状态转移。

inline void dfs(int x, int f) {
rep(i, x) {
int v = e[i].v, w = e[i].w;
if(v == f) continue;

    dfs(v, x);
    b[x] += b[v] + 1;
    fo(j, min(q, b[x]), 1, 1) {
        fo(k, min(b[v], j - 1), 0, 1) {
            dp[x][j] = max(dp[x][j], dp[x][j - k - 1] + dp[v][k] + e[i].w);
        }
    }
}

}

其中b[i]为以i为根的子树中的总边数。

例题2:P1352 没有上司的舞会

某大学有N个职员,编号为1~N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数Ri,但是呢,如果某个职员的上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

数据范围:N <= 6000

思路

我们可以抽象的把每个职工和他的上司连一条边,这样一定可以成为一棵树。既然上司和下属不能同时来,那么问题就转化成了树的最大独立集问题。

我们设 dp[i][0/1] 为第 i 个职员来/不来时其子树的最大答案。

那么 dp[i][0] 可以有两种情况,一是他的下属不来,二是他的下属来了。我们取 max ,则:

dp[i][0] = sum{max(dp[v][0], dp[v][1])} (v ∈ S)

dp[i][1] 就只能是他的下属没有来时他才能来,则

dp[i][1] = sum{dp[v][0]} + happy[i] (v ∈ S)

例题3:P2016 战略游戏

Bob喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。

他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。

注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。

请你编一程序,给定一树,帮Bob计算出他需要放置最少的士兵.

数据范围:n <= 1500

思路

一个裸的最大独立集问题,我们设 dp[i][0/1] 为选/不选这个点放士兵其子树最少需要的士兵数

那么当这个点不放士兵时,其儿子节点必定要放,则:

dp[i][0] = sum{f[v][1]} (v ∈ S)

当这个点放士兵时,其他节点可放可不放,我们取最小的就好,则:

dp[i][1] = sum{min(f[v][1], f[v][0])} (v ∈ S)

这个DP在树上做DFS即可

例题4:UVA1218 完美的服务 Perfect Service

一个网络中有N个节点,由N−1条边连通,每个节点是服务器或者客户端。如果节点u是客户端,就意味着u所连接的所有点中有且仅有一台服务器。求最少要多少台服务器才能满足要求。

数据范围:n <= 10000

思路

我们把这棵树想象成一棵有根树(对答案无影响),那么我们每一节点都可能有三种情况:

  • 这个节点有服务器
  • 这个节点没有服务器,但是这个节点的父节点有
  • 这个节点和其父亲都没有服务器

那么我们把这三种情况分别对应 dp[x][0 / 1 / 2]

  • dp[x][0]:当这个节点有服务器时,他的子节点可以有,也可以没有,也就是说:

      dp[x][0] = sum{min(dp[v][0], dp[v][1])} + 1; (v ∈ S)
  • dp[x][1]:当这个节点没有服务器且其父亲节点有服务器时,他的子节点就不能有服务器,这个时候其父节点没有,故:

      dp[x][1] = sum{dp[v][2]}
  • dp[x][2]:当这个节点父节点和本身没有服务器的话,那么它的子节点必定有且只有一个点是服务器,所以我们只能是其他的节点是客户端,剩下的一个子节点是服务器,也就是:

      dp[x][2] = min{sum{dp[v][2]}(v ∈ S && v != k)} + dp[k][0] (k ∈ S)

    我们发现这个式子可以简化,我们可以借用 dp[x][1] 来更新 dp[x][2] ,即为

      dp[x][2] = min{dp[x][1] - dp[v][2] + dp[v][0]} (v ∈ S)

5.状压DP

例题1:P1896 [SCOI2005]互不侵犯

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。

数据范围:n <= 10, K <= n * n

思路:

我们首先发现搜索是不太可能的,毕竟这个 n 有点大。我们可以考虑状压。

首先我们可以考虑如何排除不正确的情况。首先我们发现无论行与行之间怎样,在每一行中都只有那么几种状态才能保证在单行才能正确,比如:

011000 (一定是错的)

001001 (可能是对的,但需要在具体的行中作进一步判断)

这样我们可以先把每行的各种可能情况枚举出来,如果一定是错的就排除,如果可能是对的,那么说明在每行都有可能是这些情况,我们先记录下来,以便判断行与行之间的判断,顺便把每种情况用到的国王数记录下来。

然后我们可以可以设计 f[i][j][k] 为前 i 行,状态是 s[j] (这里的s[j]是指我们枚举到的合法的第 j 种单行的情况)时已经放了 k 个的情况数,我们可以首先枚举 i 行,然后枚举 s[j],然后枚举 k 个可能转移过来的数量,再枚举第 i - 1 行的情况为 s[t],假如我们判定:

!(s[t] & s[j]) && !(s[t] & (s[j] << 1)) && !(s[t] & (s[j] >> 1))

也就是说第 i - 1 行中,没有国王在第 i 行的这个 s[j] 情况下的三个可能互相攻击的位置上,则:

f[i][j][k] += f[i - 1][t][k - num[j]]

其中 num[j] 为第 j 个状态样用到的国王数。
最后把所有 f[n][i][K] 统计一下就好了。

P.S.:如果你是从信息学奥赛一本通提高版过来的话,记住:书上的样例有一个是错的:(以下的是对的)

input:
    4 4
output:
    79

例题2:P1879 [USACO06NOV]玉米田Corn Fields

n * m 个方格组成的土地,给出可以选择种草的格子,要求选出的格子不能相邻,问有多少选择方案(不选任何一个也行)

思路

经典的状压DP,很明显我们可以用二进制来表示状态。我们可以先和上一个题一样,先判断各行(这次需要每行判一次)的合法状态,然后判断行与行之间的合法关系。

我们设 f[i][j] 为前 i 行第 i 行状态为 j 时的方案数。我们不难发现,假如枚举上一行( i - 1 )的状态,假如可以到 j 这个状态,那么:

f[i][j] = sum{f[i - 1][k]} (k ∈ S && !(k & j))

例题3:P2704 [NOI2001]炮兵阵地

n * m 个方格组成的土地,给出可以选择放置炮兵的格子,要求选出的格子不能相邻或者相隔一格,问最多能放多少(不选任何一个也行)

0 0 1 0 0 
0 0 1 0 0
1 1 2 1 1
0 0 1 0 0
0 0 1 0 0 

2为炮兵,1处不能放

思路

说的不太直白点,这个题目简述我直接从上一题复制粘贴然后改了改……

同样用二进制表示状态,和上一题基本相似,但毕竟这题和两行有关,所以我们就向上扩展两行就好。

我们设 f[i][j][k] 为前 i 行中第 i 行状态为 j ,第 i - 1 行状态为 k 时的最大值,我们枚举第 i - 2 的状态 l ,转移方程直接写上:

f[i][j][k] = max{f[i - 1][k][l]} (!(j & k) && !(k & l) && !(j & l))

有点细节:我们要手动先更新第一行和第二行的 f[][][],我们要记得在预处理时把当前状态用到的炮兵数记录下来。还有:这题是求最优解,不是方案数!

6.斜率优化

例题1:P3628【APIO2010】特别行动队

给一个长度为n的序列A[],分成若干段,每分成一段,其代价是ax2+bx+c,x为这一段的和,问分割最大的代价是多少?

要求:O(n)

思路:

设dp[i]为分到第i个数时,其最大代价是多少。

我们可以枚举上一段分开的位置,即分[1,i]为[1,k],[k+1,i]两段。

其整段的和可用前缀和维护。

转移方程:

dp[i] = max{dp[j] + a*(s[i]-s[j])*(s[i]-s[j]) + b*(s[i]-s[j]) + c}
考虑优化:

我这里 f[i] 就是上述的 dp[i]……

假设 k < j < i,如果从 f[j] 转移到 f[i] 比到 f[k] 转移到 f[i] 更优,即:

f[j] + a*(s[i]-s[j])*(s[i]-s[j]) + b*(s[i]-s[j]) + c < f[k] + a*(s[i]-s[k])*(s[i]-s[k]) + b*(s[i]-s[k]) + c

化简:

f[j] + a*(s[j])^2 - 2s[i]*s[j] - b*s[j] < f[k] + a*(s[k])^2 - 2s[i]*s[k] - b*s[k]

我们把只有 j, k 的项移到一边,得:

(f[j] + a*(s[j])^2 - b*s[j]) - (f[k] + a*(s[k)^2 - b*s[j]) < 2 * s[i] * (s[j] - s[k])

将 s[j] - s[k] 除过去,得:

(f[j] + a*(s[j])^2 - b*s[j]) - (f[k] + a*(s[k)^2 - b*s[j]) / (s[j] - s[k]) < 2 * s[i] 

如果令 y[i] = f[i] + a*(s[i])^2 - b*s[i], x[i] = s[i],得:

(y[j] - y[k]) / (x[j] - x[k]) < 2 * s[i]

这个式子可以看做斜率,如果满足这个条件,也就是说从 f[j] 转移到 f[i] 比到 f[k] 转移到 f[i] 更优,我们就可以把 k 这个可转移的点删去即可,我们可以用双端队列实现。

for(int i = 1; i <= n; ++i) {
    while(L < R && slope(q[L], q[L + 1]) < l[i]) ++L;
    int j = q[L];
    f[i] = f[j] + a*x_2(s[i]-s[j]) + b*(s[i]-s[j]) + c;
    while(L < R && slope(q[R], i) < slope(q[R - 1], q[R])) --R;
    q[++R] = i;
}

愿天下OIer不向DP折腰!

转载于:https://www.cnblogs.com/yizimi/p/10254583.html

你可能感兴趣的:(动态规划:DP从入门到破门而出(入门必刷例题))