zoukankan      html  css  js  c++  java
  • 动态规划: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折腰!

  • 相关阅读:
    [CVPR2017]Online Video Object Segmentation via Convolutional Trident Network
    [CVPR2018]Context-aware Deep Feature Compression for High-speed Visual Tracking
    光栅图形学(二):圆弧的扫描转换算法
    光栅图形学(一):直线段的扫描转换算法
    Vector使用
    STL源码剖析 — 空间配置器(allocator)
    C++ traits技术浅谈
    OpenCv 2.4.9 (二) 核心函数
    vs2017 android demo
    asp.net webapi 自托管插件式服务
  • 原文地址:https://www.cnblogs.com/yizimi/p/10254583.html
Copyright © 2011-2022 走看看