DP&图论 DAY 3 上午

守給你的承諾、 提交于 2019-11-26 17:22:32

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; 
2s =s|(1<<i):把第 i 位设置成 1;
3s =s&(~(1<<i)):把第 i 位设置成 0;
(~:是按位取反,包括符号位)
4s =s^(1<<i) :把第 i 位的值取反;
5s =s & (s – 1):把一个数字 s 二进制下最靠右的第一个 1 去掉;
6for (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在集合内,则2x3x
在集合内。
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],那么xy互不
影响。对于互相有影响的一组数,一定能表示成q*2^a*3^b(q为常数)。对
每种q分别求解,再相乘即可。
q=1为例,可把所有2^a*3^ba,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-12*i个要保证之差小于等于C,才是合法的一组。
其实就是选尽量多合法的组并起来等于全集,枚举子集的状态压缩dp
可。
dp[i]=max{dp[j]+dp[i^j] | ji的一个子集}

 

 

一点总结
像这种某些集合是可行的,且每个集合有一个价值,然后我们要选择一
些不相交的集合并起来等于全集,每个选择的集合都要求可行,且希望
总权值尽量大。
这一类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是个三进制数,第i0/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<=24m<=100
>Solution

贪心来看显然要用尽量用最大的那几个, 所以先把包从大到小排序。
f[S]为把集合S放入包后至少要用到第几个包, g[S]记录此时用到的最后
一个包能剩余的最大体积。
转移时枚举接下来放哪个物品即可。

如果可以放进去,就放进去
时间复杂度O(n*2^n)

 

 

 

总结

 

 

 


 DP常见优化方式

 noip范围内的dp优化方法

◦ 加速状态转移:
1:前缀和优化
2:单调队列优化
3:树状数组或线段树优化

◦ 精简状态
4:精简状态往往是通过对题目本身性质的分析,去省掉一些冗余的状态。
        相对以上三条套路性更少,对分析能力的要求更高。

 

 

前缀和优化

逆序对

求长度为n,逆序对为k的排列有多少个,答案对10^9+7取模。
1<=NK<=200
1<=NK<=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)
其实这题复杂度还可做到nk均是100000loj60772017年山东省队集
训题)
在这个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(因为对于固定的iap[i]是固
定的),这样f[i][j]就能做到O(1)求得,而不必枚举k。卖出也一样。

 

 

 

之前讲的过一些问题
1O(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传纸条
一个mn列的矩阵,而小渊和小轩传纸条。纸条从小渊坐标(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]); 

 

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!