给中级者的动态规划教程

认识更多模型

区间DP

POJ 2955 Brackets:最简单的区间DP,括号序列

LA4394 String Painter:两层区间DP,难度较大

概率DP

POJ 3744 Scout YYF I:概率型递推配合矩阵快速幂加速

POJ 2096 Collecting Bugs:优惠券问题的二维情形

状态压缩DP

UVA 10911 Forming Quiz Teams:最优配对问题

LA3725 Hie with the Pie:TSP问题

树DP

POJ 1655 Balancing Act:求树的重心

Codeforces 461B Appleman and Tree:最大独立集问题的变种

 

 

LA4394 String Painter

题意

给定两个长度相等,只有小写字母构成的字符串s和t,每步可以把s的一个连续子串“刷”成同一个字母,问至少多少步才能把s变成t。

思路

两层区间DP。

首先忽略原始串s,只去看目标串t,设dp1[i][j]表示从什么都没有的情况下利用刷子规则构造出目标串t的i到j这一段所需要的最小操作次数。这里着眼于“最后一次操作”进行阶段的划分。

dp1的状态转移:

  • dp[i][j] = dp[i+1][j ] + 1
    先默认最后一次操作是只刷出最新那个字符。
  • 令k是i和j中间的某个位置,t是目标串,如果t[i] == t[k]
    尝试用dp[i+1][k] + dp[k+1][j]更新dp[i][j]
    因为i处字符等于k处字符,在构造i+1到k这个区段的时候,可以在刷k处字符的同时“顺便”把i位置也给刷了。
    注意这里不可以写成dp[i+1][k-1] + 1 + dp[k+1][j],考虑反例” /e(i对应)/ee/e(k对应)/… “,在这里dp[i+1][k-1]是1,用这种错误的算法dp[i][k]部分会变成2,但实际上应该是1。

接下来引入原始串s,令dp2[i]表示把原始串1到i构造成目标串1到i的部分所需要的最小的操作次数。这里有状态转移:

  • 默认dp2 = dp1[1][i]
  • 如果s[i] == t[i]
    尝试dp2[i] = dp2[i-1]
    否则尝试dp2[i] = dp2[i-1] + 1

    这里的意思是如果在第i位两个串对应字符相等,可以选择无需对这一位进行操作

  • 令k是1到i之间的位置
    尝试dp2[i] = dp2[k-1] + dp1[k][j]

    这里的意思是假设最后k位使用不考虑原始状态的构造方法,前面继承最优解。为什么要这样做?考虑fabcf这样的区间,一种较好的策略是先都刷成f,然后分别刷好abc,如果你选择这么做了,相当于已经无视了原来串的样子,因为第一步已经将全部中间字符都刷成f了。

代码

 

LA3725 Hie with the Pie

题意

经典问题:TSP。

给出了n个城市,求一条最短的路径,使得经过每个城市一次且仅一次,最后回到起点。

思路

状态压缩DP。分析问题可知道是一个多阶段决策过程,每个阶段“状态”包括了可以用已经走了哪些点,和当前站在哪个点,决策就是接下来走哪个点。

状态表示:dp[二进制位,表示目前已经走过那些点][最后走的点的序号] = 从起点出发走过的最短路程

答案:min{dp[走过全部点的状态][枚举各个可能的最后走的点i] + dis(i,起点)}

边界:dp[0][起点] = 0,其它都是INF

状态转移:用dp[S][i] + dis(i,j) 去尝试更新dp[S|j][j]

代码

 

UVA 10911 Forming Quiz Teams

题意

经典模型“最优配对问题”,给出了平面上若干个点,你要把牠们两两配对,使得每一对中两点距离之和最小。

思路

分析问题:我们发现,本问题可以着眼于“最后一次配对”,是一个多阶段决策。不妨考虑当最后一次选择拿i号点去和j号点去配对,得到了某种状态时可以得到的最优解。

状态表示:dp[当前状态下各个点是否已经配对][最后一次配对的点i][最后一次配对的点j]

我们发现这个状态设的太繁了,假如我们要配对a和b这两个点,那么在所有的配对状态里面都要算入a和b的距离,在所有没配对状态里都不可以算入a和b的距离,后两个维度强行决定了一组点的配对情况,然而并没有什么意义,因为后两个维度的信息已经包括进了第一个维度里,我们只要想办法让第一个维度可以表达出决策的“分阶段”特性,能够识别出最后一步是什么就可以。

于是我们重设状态:dp[当前状态下各个点是否已经配对]并且规定在每个状态中,最低位为1对应的那个点是我们最后一次操作的点。

这样有状态转移:dp[S] = min{dis(i,j) + dp[S^i^j]} 在这里S是二进制表示的状态,i,j是假定的最后一次配对的点对,其中规定i一定是S中最低位的那个点,dis表示求距离,S^i^j表示将二进制表示的状态中i,j对应的位设成零之后得到的二进制状态

代码

 

POJ 2955 Brackets

题意

给出一个由小括号喝中括号组成的括号序列,定义正规括号序列为(),[],(s),[s],其中s也是正规括号序列。求最长正规括号序列的长度。

思路

考虑区间DP,设计状态时着眼于“最后一次加括号”。

状态表示:dp[i][j] = 从 i 到 j 的(包含i,j)最长的括号序列长度。

状态转移:

  • dp[i][j] = dp[i][j-1] 继承上一个区间的结果
  • 令k为i,j之间的一个位置,str是给出的括号串(令下标从1开始)
  • 如果str[k]与str[j]正好是一对匹配的括号,可以考虑在最后一步放这个括号,用dp[i][k] + 2 + dp[k+1][j-1]尝试更新dp[i][j]

答案:dp[1][len]

初始化:直接令整个dp数组都为0,既可以完成初始化,也可以无效化所有无效状态。

代码

 

位运算有关小技巧

基本运算

以下a指一个二进制位。

NOT

  • ~1 = 0
  • ~0 = 1

如果智商正常应该不会不能理解取反运算吧

AND

  • 1&a = a
  • 0&a=0

由此可见善加利用AND运算,可以保证某个二进制数的特定位为0,其余的位保持原样。

OR

  • 1|a=1
  • 0|a=0

由此可见善加利用OR运算,可以保证某个二进制数特定位为1,其余位维持原样。

XOR

XOR/异或运算具有重要性质:

  • a^a = 0
    XOR可以代替等于
  • 0^a = a
    0是XOR运算的单位元
  • 1^1=0 1^0=1
    用1去做XOR运算可以实现“开关”某个二进制位的效果
  • abs(a^1-a)=1
    用1对一个数去做XOR可以实现求与这个数相邻的数的效果
    比如2^1=3 3^1=2
  • 1^1 = 0;1^1^1 = 1;1^1^1^1 = 0
    XOR与奇偶性紧密相连

SHL/SHR

左移运算相当于除以2,右移运算相当于乘2.

运算技巧

scanf

while(~scanf(” %d”,&n)){}
while(scanf(” %d”,&n) != EOF){}

两种形式等价

判断奇偶性

if a&1 == 1 then a is odd
if a&1 == 0 then a is even

取出最低位

lowbit(x) = x&(-x)

制作全1掩码

mask = (1 << n) – 1

理论最大最小值

INTMAX = ~(1 << 31)
LLMAX = ~(1ll << 63)

INTMIN = 1 << 31
LLMIN = 1ll << 63

常用最大值

memset(dp,0x3f,sizeof(dp))

装逼

swap

a= a ^ b
b = a ^ b
a = a ^ b

绝对值

abs(n) = (n ^ (n >> 31)) – (n >> 31)

最大值

max(a,b) = b & ((a-b) >> 31) | a & (~(a-b) >> 31)

最小值

min(a,b) = a & ((a-b) >> 31) | b & (~(a-b) >> 31)

集合

表示集合

用二进制位表示集合是很简单也是很直观的,直接举个例子:假设全集是{a,b,c,d,e,f,g,h},这样我们一共有8种不同的元素,怎么表示{a,c,e}这个集合呢,很简单,我们拿出8个二进制位,从左到右分别表示“a,b,c,d,e,f,g,h”,那么{a,c,e}就可以用二进制”10101000″ 表示。

操作用二进制表示的集合

加入元素:使用“或等”运算

删除元素:使用“和等”运算

确认元素是否在内:使用“和”运算

并集:使用“或”运算

交集:使用“和”运算

补集:先使用“取反”运算,再使用“和”运算

开关:使用“异或”运算

枚举

枚举全部的集合形态:从0枚举至11111111…

枚举某个集合的自己:-1,然后和求子集的集合做“和等”

图论

获取反向边

i i^1互为反向边,要求插边时必须同时插入正边和反边,边数组编号从2的倍数或0开始。

数据结构

二叉树

令根节点为1

编号x的节点的左儿子编号: (x << 1)
编号x的节点的右儿子编号: (x << 1) + 1
编号x的节点的父亲编号: (x >> 1)
编号x的节点的兄弟编号: (x ^ 1)

二叉树两个节点节点是否相邻

使用堆存储,根节点编号为1,如果a^b=1,则两个点必定同属一个父亲

二叉堆

先坑着,看需求

动态规划

滚动数组

模运算很慢,所以我们用XOR代替mod2。

int tick = 0;
for(){
dp[tick^1] = dp[tick];
tick ^= 1;
}

快速幂

这里暂时不详细提供代码了。

 

Scroll to top