DP&图论 DAY 3 上午
状态压缩dp
>状态压缩dp
◦状态压缩是设计dp状态的一种方式。
◦当普通的dp状态维数很多(或者说维数与输入数据有关),但每一维总
量很少是,可以将多维状态压缩为一维来记录。
◦这种题目最明显的特征就是: 都存在某一给定信息的范围非常小(在20
以内),而我们在dp中所谓压缩的就是这一信息。
◦(或者是在做题过程中分析出了某一信息种类数很少)
◦我们来看个例子。
>经典题
◦给出一个n*m的棋盘,要放上一些棋子,要求不能有任意两个棋子相邻。
求方案数。
◦ n<=100;
◦ m<=8。
>Solution
dp[i][0/1][0/1]....[0/1]
第 i 行每一列的状态0/1
然后考虑第i+1行,如果不冲突,转移
然后发现多维可以压缩成一个数dp[i][s]
dp[i][s]-->dp[i+1][s'] s‘和s不冲突,&一下=0
◦我们发现这个m是非常小的,这样就可以启发我们对每一行2^m状态压缩。
◦设dp[i][S]表示到了第i行,第i行的状态是S的方案数是多少。
◦其中S的第j位为1,表示i这行第j位放了一个棋子。
◦状态转移: dp[i][S]=sigema {dp[i-1][ S' ] | S&S' ==0 }。
◦你会发现这样记录很暴力,状态数是与m相关的指数级的,但同时也就是
因为m小我们就确实可以这么做。
◦这也正好映照了之前的:
O(n*2^8*2^8)
(后来你发现它可以斐波那契数列)
状态压缩dp
◦其实本质就是很暴力的记录状态,只不过利用了题目本身的特殊条件
(这一维很小),使得我们并不会因此复杂度过高。
◦同时也就是说,如果题目本身没有这样一个较小的信息,就不能应用状
态压缩。
◦所以在接下来的题目中大家可以更注意一下题目所给的条件。
◦状态压缩dp肯定是有一维是指数级的,这正是状态压缩的特点。
>BZOJ1087 互不侵犯
◦在n*n的棋盘上放置k个国王,使得任意两个国王互相不攻击。一个国王
可以攻击到周围八个格子。
◦求放置方案数。
◦ n<=9
>Solution
dp[i][j][s]-->dp[i+1][j+bit(s')[s']
咋判断??
不移 左移 右移
上下 八连通
◦ f[i][k][S]表示k个放完前i行,第i行的放置状态为S的方案数。
◦ S为一个n位二进制数,表示第i行的每一位上是否有棋子。
◦由于棋子不能相邻,也就是这个二进制不能有相邻的1,所以可以先对求
出对一行来说合法的状态(合法的二进制数有哪些)。
◦转移: f[i][k+cnt(S)][S]+=f[i-1][k][T], ( (T|(T<<1)|(T>>1)) & S )==0
◦ (巧用位运算! )
◦时间复杂度O(n^3*状态数^2)<O(n^3*2^(2n))
◦(实际上状态数是 Fibonacci)
位运算在这里发挥了大的用途
◦状态压缩有k进制的,论题目出现频率的话二进制的题目居多,同时因为
计算机本身存储数就是以二进制方式,所以二的幂次的进制处理很方便,
状态压缩dp中有时巧用位运算可以产生很好的效果,比如本题,我们就
不需要O(n)枚举每一位判断是否冲突了。
常用位运算
◦ 1、 (s & (1 << i)):判断第 i 位是否是 1;
◦ 2、 s =s|(1<<i):把第 i 位设置成 1;
◦ 3、 s =s&(~(1<<i)):把第 i 位设置成 0;
◦ (~:是按位取反,包括符号位)
◦ 4、 s =s^(1<<i) :把第 i 位的值取反;
◦ 5、 s =s & (s – 1):把一个数字 s 二进制下最靠右的第一个 1 去掉;
◦ 6、 for (s0 = s; s0; s0 = (s0 - 1) & s): 依次枚举 s 的子集;
(从大到小枚举)
◦这个二重循环的复杂度是多少?怎么推?
O(3^n)
。7: x&-x 取出x最低位的1。
取反+1
>拓扑序个数问题
。给你一张拓扑图,求这张拓扑图有多少种不同的拓扑序.
n<=20。
>Solution
dp[s] s状态的方案
ans=dp[ (1<<n ) -1]
2^n * n * 判断
预处理in[u]成为一个二进制数,比如in[u]=10010
考虑加进去一个新点,那么他的入度都被加入了
S&in[u]=in[u] 那么就可以加入这个点
◦ dp[s]表示当前s集合中的点都已经在拓扑序中的方案数,转移考虑枚举下一
个点选什么,下一个选的点要满足它在s中的点选完后的入度为0,也就是
指向它的点都已经加进拓扑序里了,转移到dp[s|(1<<i-1)] 。
◦这个有没有位运算的优化呢?
◦复杂度O(2^n*n)
>bzoj2734
◦给定n,问集合{1,2,3,…,n}有多少个子集满足:若x在集合内,则2x和3x不
在集合内。
◦ n<=100000
>Solution
对于每一个数
都可以拆分成 q * 2^x * 3^y
不同q的数字一定不同
1 | 31 | 32 | 33 | 34 | 35 |
21 | 21*31 | 21*32 | 21*33 |
21*34 | 21*35 |
22 | 22*31 | 22*32 | 22*33 | 22*34 | 22*35 |
23 | 23*31 | 23*32 | 23*33 | 23*34 | 23*35 |
24 | 24*31 | 24*32 | 24*33 | 24*34 | 24*35 |
25 | 25*31 | 25*32 | 25*33 | 25*34 | 25*35 |
任意相邻元素不可选
dp[i][s]
◦记g[x]表示将x中的2,3因子去除后得到的值,若g[x]!=g[y],那么x与y互不
影响。对于互相有影响的一组数,一定能表示成q*2^a*3^b(q为常数)。对
每种q分别求解,再相乘即可。
◦以q=1为例,可把所有2^a*3^b以a,b为行列画成一个三角形棋盘,求棋盘
上放不能相邻的棋子的方案数。我们会发现这个三角形最宽的层最多只
有11个,所以可以状态压缩。
◦这也就对应了
◦当q>1,等价于q'=1&&n'=n/q。
◦剪枝:对于n/q相同的q,可以避免重复计算来加速。
打表
我们看到最宽的一层第10层宽度只有11,这个范围显然是可以
2^cnt_floor状态压缩。
>NOIP2016 愤怒的小鸟
◦平面上有n头猪,每次可以从(0,0)出发发射一只沿抛物线(y=ax^2+bx)飞行
的小鸟,可以消灭所有在飞行路线上的猪。
◦问消灭所有猪至少要几只小鸟。
◦ n<=18
>Solution
N^2个抛物线,t[i]每条抛物线消灭的猪
dp[s]=min{dp[s^t[i]+1 | t[i]&s=t[i],t[i]包含s的最低位}
◦两头猪加上原点即可确定抛物线,于是不同的抛物线只有O(n^2)种。
◦设f[S]为已经消灭的猪的集合为S时的最少次数,暴力的转移方法为依次枚
举抛物线去更新所有f,这样做时间复杂度为O(n^2*2^n)。
◦更快的转移方法为从小到大枚举S,每次打掉编号最小的还没消灭的猪,
由于包含该猪的抛物线只有O(n)种,所以时间复杂度为O(n*2^n)。
◦其实就避免一些重复的计算。
> poj2923
◦有 n 个货物,并且知道了每个货物的重量,每次用载重量分别为c1,c2的
火车装载,问最少需要运送多少次可以将货物运完。
◦ N<=17
>Solution
假设我们求出了哪些货物可以一次运走,就转化为上一个题
dp[s]=min(t){dp[s^t]+1 | t&s==t} s的子集t(可以强制t包含s的最低位)
f[s] s可否被c1运走 0/1
g[s] s可否被c2运走 0/1
t[s]= | (s') f[s']&g[s&s'] s能否被两辆车运走
◦找出所有状态(1.....(1<<n)-1)中选出可以用两辆车一次运完的状态(枚
举全部)
◦把每个状态都看作一个物品,重量为该状态的总重量,价值为 1,其实我
们就是求最少选几个不相交的集合能够覆盖全集。
◦求解 01 背包, dp[(1<<n)-1]为最优解。
◦转移方程: dp[s]=min(dp[s],dp[s^t]+1) 其中t集合内的元素可以被一次运
完。
◦这个复杂度是O(3^n)。
◦实际上如果我们固定t集合包含s最低位的1,这样能避免重复运算,复杂
度降约为O(3^(n-1))
枚举子集3^n也是很常见的状压dp!!
>Bzoj3900
◦动物园里有 n 头麋鹿。每头麋鹿有两支茸角,每支茸角有一个重量。然
而,一旦某头麋鹿上两支茸角的重量之差过大,这头麋鹿就会失去平衡
摔倒。为了不然这种悲剧发生,动物园院长决定交换某些茸角,使得任
意一头麋鹿的两角重量差不超过 c。然而,交换两支茸角十分麻烦,不
◦仅因为茸角需要多个人来搬运,而且会给麋鹿造成痛苦。因此,你需要
计算出最少交换次数,使得任意一头麋鹿的两角重量差不超过 c。
◦注意,交换两支茸角只能在两头麋鹿之间进行。因为交换同一头麋鹿的
两支角是没有意义的。
◦对于 100% 的数据, n <= 16, c <= 1000000, 每支茸角重量不超过 1000000。
>Solution
把 2t 个茸角排序,如果相邻两个不能匹配c,那么就尝试交换与他相邻的那一对
发现交换上限是t-1,因为每次交换可以使得一对合法
f[s]=min(s'∈s) min(f[s']+f[s^s'])
◦首先其实最终肯定是把这些鹿分成一些组,每一组内通过组的大小减一
次操作来满足题目要求的条件。注意对于一个组,我们将所有的角排序,
第2*i-1和2*i个要保证之差小于等于C,才是合法的一组。
◦其实就是选尽量多合法的组并起来等于全集,枚举子集的状态压缩dp即
可。
◦ dp[i]=max{dp[j]+dp[i^j] | j是i的一个子集}。
一点总结
◦像这种某些集合是可行的,且每个集合有一个价值,然后我们要选择一
些不相交的集合并起来等于全集,每个选择的集合都要求可行,且希望
总权值尽量大。
◦这一类dp往往是要用到状态压缩dp同时利用枚举子集来进行转移,转移
的过程中常常可以控制最低位的1必选来减少dp重复的计算。像这前两道
题,以及之前的愤怒的小鸟也一样,只不过小鸟那题通过分析题目的特
点使得我们在转移的过程中不需要枚举全部的子集,只需要枚举n个抛物
线即可。
>Hdu3001放松题
◦现在有一个具有n个顶点和m条边的无向图(每条边都有一个距离权值),
小明可以从任意的顶点出发,他想走过所有的顶点而且要求走的总距离
最小,并且他要求过程中走过任何一个点的次数不超过2次。
◦ 1<=n<=10
>Solution
dp[s][u]-->
min dp[s][u]
3^n * n * n
三进制状态压缩
◦设dp[S][i]表示到过点的情况集合为S,S是个三进制数,第i为0/1/2分别表示
到过次数。 i为当前所在的点。转移的话就考虑下一步走到那条边就好。
◦答案就是dp[3^n-1][i]的最小值。
◦会二进制的表示,三进制想起来应该很自然吧。
◦其实是TSP旅行商问题的一种较为麻烦的形式。
关于时间复杂度
◦ N=20一般是2^n或者n*2^n。
◦ N<=16大概率是3^n,约是4*10^7,那很可能做法就和之前讲枚举子集有
关了。
◦ N<=15大概率是3^n或者n*3^n。
>BZOJ3717 Pakowanie
◦给出n个物品和m个包,物品有各自的体积,包有各自的容量。
◦问将所有物品装入包中需要至少几个包。
◦ n<=24, m<=100。
>Solution
◦贪心来看显然要用尽量用最大的那几个, 所以先把包从大到小排序。
◦设f[S]为把集合S放入包后至少要用到第几个包, g[S]记录此时用到的最后
一个包能剩余的最大体积。
◦转移时枚举接下来放哪个物品即可。
如果可以放进去,就放进去
◦时间复杂度O(n*2^n)
总结
DP常见优化方式
noip范围内的dp优化方法
◦ 加速状态转移:
◦ 1:前缀和优化
◦ 2:单调队列优化
◦ 3:树状数组或线段树优化
◦ 精简状态
◦ 4:精简状态往往是通过对题目本身性质的分析,去省掉一些冗余的状态。
相对以上三条套路性更少,对分析能力的要求更高。
前缀和优化
> 逆序对
◦求长度为n,逆序对为k的排列有多少个,答案对10^9+7取模。
◦ 1<=N, K<=200
◦ 1<=N, K<=2000
>Solution
dp[i][j]前i个数j个逆序对
dp[i][j]=dp[i-1][j]+dp[i-1][j-1]
sigema (k=0~i-1) dp[i-1][j-k]
=sum[i-1][j]-sum[i-1][j-i]
sum[i][j]=sigema(k=0~i) dp[i][k]
◦排列题的一个套路,我们从小到大依次把数字插入到排列中,以这
个思路进行dp。
◦这个问题设计动态规划的方法就是最基本,最自然的。
◦我们设dp[i][j]表示插入了前i个数,产生的逆序对为j的排列的方案数,
转移时就考虑i+1的那个数插在哪一个位置就好,因为它比之前的都
要大,插在最后面就dp[i+1][j+0]+=dp[i][j],如果时最后一个数前面就是
dp[i+1][j+1]+=dp[i][j],倒数第2个数前面就是
◦ dp[i+1][j+2]+=dp[i][j],依次类推。 这个是从前向后更新。
◦我们如果考虑dp[i][j]能从哪些状态转移过来,就可以前缀和优化。
◦方程:
◦我们设:
◦则dp[i][j]=f[i-1][j]-f[i-1][j-i]
◦这样我们通过记录前缀和,将转移优化成O(1)
◦其实这题复杂度还可做到n和k均是100000: loj6077(2017年山东省队集
训题)
◦在这个dp基础上,做容斥原理,通过之前讲的整数划分的模型dp求出容
斥系数即可。
单调队列优化
基本形式,适用范围
◦单调队列维护dp,一般就是把一个O(N)转移的dp通过单调队列优化成一
个均摊O(1)转移的式子。
◦式子形如: dp[i]=max{f(j) }+g[i](这里的g[i]是与j无关的量),且j的取值是
一段连续区间, 区间端点的两端随着i增大而增大的区间。
◦(同时如果这个可行的区间左端点固定,我们就可以通过之前讲的记录
前缀最小值来处理)
◦这里的f(j)是仅和j有关的项。以下是常见的一维和二维的情况。
◦ dp[i]=max{dp[j]+f(j)}+g[i] 或者 dp[level][i]=max{dp[level-1][j]+f(j)}+g(i)
◦这样的题我们就可以做单调队列优化。
>bzoj1855: 股票交易
◦我们考虑在未来T天内的某只股票,第i天的买入价为每股APi,第i天的卖
出价为每股BPi(对于每个i,都有APi>=BPi),每天不能无限制地交易,
规定第i天至多只能购买ASi股,至多卖出BSi股。另外规定在两次交易(买
入或者卖出均算交易)之间,至少要间隔W天,也就是说如果在第i天发
生了交易,那么从第i+1天到第i+W天,均不能发生交易。还规定在任何时
间,一个人的手里的股票数不能超过MaxP。
◦初始w手里有一大笔钱(可以认为钱的数目无限),但是没有任何股票,
问T天以后,小w最多能赚多少钱。
◦ 0<=W<T<=2000
>Solution
◦设f[i][j]表示到第i天手里持有j的股票的最大收益,那么第i天有三种操作:
◦对于买入,我们对其变形:
◦那么可以用单调队列维护f[i-w-1][k]+ap[i]*k(因为对于固定的i, ap[i]是固
定的),这样f[i][j]就能做到O(1)求得,而不必枚举k。卖出也一样。
之前讲的过一些问题
◦ 1: O(N*M)的多重背包。
◦ 2:基环树求直径。
单调队列优化和前缀和优化的对比
◦单调队列优化dp跟前缀和优化dp差不多一个思路,转移的合法点集都是
一个区间。
◦只不过,单调队列优化dp是当你在最优化dp值时候和一段区间有关。
◦而,前缀和优化dp是在计数的时候和一段区间有关。
>bzoj3831: [Poi2014]Little Bird
◦有一排n棵树,第i棵树的高度是Di。
◦ MHY要从第一棵树到第n棵树去找他的妹子玩。
◦如果MHY在第i棵树,那么他可以跳到第i+1,i+2,...,i+k棵树。
◦如果MHY跳到一棵不矮于当前树的树,那么他的劳累值会+1,否则不会。
◦为了有体力和妹子玩, MHY要最小化劳累值。
◦ N(2<=N<=1 000 000)
>Solution
◦根据题意,我们很容易得出下面的转移方程
◦ 1.f[i]=min(f[j]+1) ( i-k≤j<i )
2.f[i]=min(f[j]) ( i-k≤j<i &&h[j]>h[i])
◦发现上面那个东西用单调队列直接搞定,但下面那个不太好搞。不过发
现由于h[j]>h[i]对答案的贡献至多为1,所以原来如果f[j]<f[j‘],那么算上h[j]
和h[j’]的影响后j仍然不会比j‘更差,于是直接维护一个f递增的单调队列,
其中当f相同的时候使h递减就行了。
◦这么做,主要利用了题目中高和矮转移的费用只差1,这样如果矮的f[i]小
的话,就算加上1也只是和高的f值相同不会更差。如果费用更高就不能
用这个方法了。
树状数组或线段树优化
>送你一堆区间
◦送你在数轴上的 n 个区间和 m 个关键点, 你可以决定每个区间选或不选,
问有多少种方案覆盖 所有的关键点.
◦对 1000000009 取模.
◦ N<=5*10^5
>Solution
首先对区间离散化,再按左端点从小到大排序,若左端点相同,就按右端点从小到大排序
考虑计算 dp[i] 表示前 i 个关键点都完全覆盖的方案
对于一个区间 [ L , R ] ,他可以将 dp[L-1]......dp[R-1]转移到dp(R)上;还可以将dp(R)....dp(m)通通乘上一个2,用线段树维护即可
后记
◦这三个技巧往往就是dp列出式子来之后,观察一下式子,你发现它满足
对应优化的模型,所以我们就单调队列或者前缀和或者线段树优化了。
◦对思维的要求并不高。
分析题目性质精简状态
精简状态
◦这个对题目性质的分析能力要求还是比较高的。
◦需要挖掘题目的性质,特点等等。
◦不能算很套路的内容,需要多做题感受。
>Noip2008传纸条
◦一个m行n列的矩阵,而小渊和小轩传纸条。纸条从小渊坐标(1,1),传到
小轩坐标(m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小
轩传给小渊的纸条只可以向上或者向左传递。
◦班里每个同学都可以帮他们传递,但只会帮他们一次。
◦全班每个同学有一个好心程度。小渊和小轩希望尽可能找好心程度高的
同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学
的好心程度之和最大。
◦ n,m<=300。
>Solution
◦其实可以理解为从小渊到小轩传两次。
◦最暴力的做法:设dp[i][j][k][l]是第一张纸条到达(i,j),第二张到达(k,l)时最
大权值。那么方程就是
◦ dp[i][j][k][l] = map[i][j] + map[k][l] + max(dp[i - 1][j][k - 1][l],dp[i -1][j][k][l - 1],dp[i][j - 1][k -1][l],dp[i][j -1][k][l -1]);
◦还有一点注意的是,如果i == k && j == l,也就是第二张走了第一张的路径,
那么就要减去, dp[i][j][k][l] -= map[i][j];
◦接下来枚举每个i,j,k,l,最后输出dp[m][n][m][n]就行了,但这样做时间复
杂度是O(n^4)
考虑优化
◦其实我们可以让两个路线并行走,同时走。
◦而既然第一张与第二张是同时走,那么我们知道他们的步数是一样的,
步数 = 横坐标+纵坐标,所以只需枚举i,j,k,就能计算出l,只需三重循环,
时间就变成了O(n^3);
◦ dp[i][j][k] = map[i][j] + map[k][i+j-k] + max(dp[i - 1][j][k - 1],dp[i -1][j][k],dp[i][j- 1][k -1],dp[i][j -1][k]);