[文文殿下]基本的DP技巧
.
二进制状态压缩动态规划
对于某些情况,如果题目中所给的限制数目比较小,我们可以尝试状态压缩动态规划。例如,题目中给出数据范围\(n<=20\),这个一般情况下是一个状压DP的提示。
状态压缩,顾名思义,要把每种状态压缩起来。一个经典的问题是洛谷P1171,也就是著名的货郎担问题,它是一个NPC难题,目前不存在多项式算法。当题目中\(n\)的范围比较小时,我们可以考虑使用状态压缩动态规划(状压DP)来解决。(注:本文出现的"状压DP"若无特殊说明,均指状态压缩动态规划)
我们用状压DP解决货郎担问题的时间复杂度是\(O(n^{2}2^{n})\),我们用\(dp[i][j]\)来表示目前处在第\(i\)个城市,集合\(j\)中的城市已经全部都经过一次所花费的最小代价。那么,由于\(j\)的范围是\(2^{n}\)(每个城市都有两种状态,共\(n\)个城市),而\(i\)的取值范围是\(n\),所以一共有\(2^{n}n\)种状态,每种状态可以出发去其他任何一个城市,所以有\(n\)种决策,所以总时间复杂度为\(O(n^{2}2^{n})\)。
通常,我们使用状压DP的时候,把集合用一个\(int\)型整数表示,它通常取值为\([0,2^{n}-1]\),用来表示每个元素的两种状态,从而表示出当前状态。我们怎么知道元素\(i\)是否在这个集合中呢?我们可以用1<<i-1来表示,这种表示方法可以查出得到元素\(i\)所代表的那一位,然后我们就可以用位运算符\(\&\)来于集合取一个交集,如果返回为真,那么说明元素\(i\)在原集合中,否则不在。
状态剪枝
普通的动态规划通常有很多个状态,而这些状态会占用大量内存以及消耗时间。有的时候,我们没必要真的去计算每一个状态,因为有的状态永远也无法转移到答案。
我们结合一道例题具体分析.
显然,我们有一个思路:用\(f[i][j]\)来表示我们在岛屿上\(i\)处,我们上一次跳跃距离是\(j\),那么我们就可以很方便的转移了,时间复杂度\(O(n^2)\),空间复杂度\(n^2\)。
现在,出题人想卡掉这种做法。这种做法的复杂之处在于:有一些状态,我们永远也无法访问,但我们还是记录并从他向外界转移了。这不是我们希望的,而我们观察到有用的\(j\)只存在于一定范围内。这个发现可以让我们减少自己的决策数,实际上,打表发现,有用的\(j\)的分布只存在于\([1,\sqrt{n}]\)中,所以我们可以进一步优化到\(O(n\sqrt{n})\).
这种优化实际上是一种直觉,我们看到\(n\)的范围是\(10000\),我们必须想办法优化,DP优化的一般思路是:打表->发现规律->利用规律->AC.
** 改变DP对象**
这道题,如果\(H,W<3000\),那么我们可以很方便的用一般的\(2D\)状态的DP来做。时间复杂度\(O(H*W)\) __ , 但是这道题的H,W太大了,我们只好考虑别的方法。
我们观察到,不能走的点很少,只有\(2000\)个,我们打算从他入手。
我们把所有的不能走的点,把\(x\)作为第一关键字,\(y\)作为第二关键字,进行排序,同时我们让点\((H,W)\)也加进来,显然他会排在最后一个。
现在,我们用\(dp[i]\)表示到达第\(i\)个不能走的点的路径条数(假设第i个格子可以走),注意到我们是根据横纵坐标递增排序的,所以对于两个数\(i,j\) 如果\(i<j\)那么一定无法从\(j\)到达\(i\),这确保了无后效性。
我们考虑两个点\((X_1,Y_1),(X_2,Y_2)\),如果中间不含任何无法走的方块,那么一共有\(\frac{(X_2-X_1+Y_2-Y_1-2)!}{(X_2-X_1+1)!(Y_2-Y_1+1)!}\)种方法。
我们用\(W[x][y]\)表示\(x->y\)的路径条数,也就是上面那个式子,我们可以通过预处理阶乘的方法,那么对于每个\(W\)我们可以O(1)求,但是\(W[0][n+1]\)并不是答案,因为经过了不能走的点。
考虑如何计算\(dp[i]\),对于没有任何限制,那么它为\(W[0][i]\),我们考虑,如果第一个遇到的不可走的点为\(j\),那么接下来走到\(i\)的方案数为\(dp[j]*W[j][i]\),我们把这个数从中减去就好啦!答案存在\(dp[n+1]\)中。
差分dp
对于这个题,我们不是太好设定状态呢。我们首先先给所有的物品排序,按照价格从小到大。那么,我们可以把他们放在数轴上,作为数轴上的点。
每个集合就是一条线段咯!而我们要求的值,就是每条线段的长度之和。因为,最左边的是价值最低的,最右边的是价值最高的。那么,我们可以这么设计状态。
\(dp[i][j][k]\)表示,当前已经放置了\(i\)个商品,也就是有\(i\)个点都已经属于某条线段了,还有\(j\)条线段只有一个端点的方案数,那么,我们每次更新一个点,都会对\(k\)产生一点贡献,这个贡献是多少呢?\(j*(a_i-a_{i-1})\).为什么是\(j\)乘呢?我们每次不是只放在了一组里吗?原因是,如果每次只更新单租的贡献,那么状态不好转移。我们不知道这一组之前上一个是谁。
也就是说,我们每次一个点更新以后,就把将来一定会用到的值更新。因为我们是已经排好序了,所以这么做是没问题的。
那么,每个状态\(dp[i][j][k]\)都有以下几个转移方式转移而来:
1.我们让商品\(i\)开一条线段,那么\(dp[i][j+1][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]\)
2.我们让\(i\)单独成一组,那么\(dp[i][j][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]\)
3.我们让\(i\)结束一条线段,此时须保证\(j!=0\),那么\(dp[i][j-1][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]*j\)
4.我们把\(i\)加入到某一条线段里(但不结束这条线段),那么\(dp[i][j][k+j*(a_i-a_{i-1})]+=dp[i-1][j][k]*j\)
最终答案存在\(\sum_{i=0}^{k}dp[n][0][i]\)
连通块dp
文文也不知道该怎么给这种dp取名,目前还没遇到过这样的题目。
例题:
给出\(n\)个数字\(a_1...a_n\),以及一个整数\(L\),\(n<=100,a_i<=1000,L<=1000\),求有多少种排列,满足\(|a_1-a_2|+|a_3-a_4|+....+|a_{n-1}-a_n|<=L\).
这道题我们很难直接设状态,我们先把他们排一遍,然后把他们一个一个加入到排列中。每加一次,都统计一下答案。例如:2,7,?,5,6,?,?,?,?,9
还没有填数的地方用?来表示,假设我们已经把前\(i-1\)个数全部填进去了,现在考虑第\(i\)个,由于是排好序的,第\(i\)个一定大于其中任意一个.
我们用\(dp[i][j][k][l]\)来表示当:
填入数字个数为\(i\),连通块个数为\(j\),当前的代价为\(k\),连通块结尾是否已经全部填充(l=0 没有, l=1一部分 l=2 全部填充)的方案总数
小细节:
1.每填入一个元素,都要更新答案的值为新造成的差值的绝对值乘上连通块的个数(因为连通块是等价的,可以调换位置)
2.每个元素,要么合并两个连通块,要么新建一个连通块,要么插在某个连通块开头或结尾
转移方式结合在代码中详细解释
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> ii;
typedef vector<int> vi;
typedef vector<ii> vii;
typedef long double ld;
#define fi first
#define se second
#define pb push_back
#define mp make_pair
ll dp[101][101][1001][3];
ll a[101];
const ll MOD = 1e9 + 7;
int main()
{
ios_base::sync_with_stdio(0); cin.tie(0);
int n, l;
cin>>n>>l;
for(int i = 0; i < n; i++)
{
cin>>a[i];
}
sort(a, a + n);
if(n == 1) //特殊情况
{
cout << 1;
return 0;
}
a[n] = 10000; //无穷大
if(a[1] - a[0] <= l) dp[1][1][a[1] - a[0]][1] = 2; //在其中一个终止点填入a[0],还有两个终止点等待填充
if(2*(a[1] - a[0]) <= l) dp[1][1][2*(a[1] - a[0])][0] = 1;
for(int i = 1; i < n; i++)
{
int diff = a[i + 1] - a[i]; //如果i=n-1 diff = inf
for(int j = 1; j <= i; j++)
{
for(int k = 0; k <= l; k++)
{
for(int z = 0; z < 3; z++)
{
if(!dp[i][j][k][z]) continue; //值不存在
//首先尝试填充其中一个端点
if(z < 2 && k + diff*(2*j - z - 1) <= l) //有2j-z-1个位置想要更优(因为这些位置中的某一个将在这一步以后与一个终止点合并)
{
if(i == n - 1)
{
dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] = (dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] + dp[i][j][k][z]*(2-z)*j)%MOD;//我们有j个连通块可以合并
}
else if(z == 0 || j > 1) //i==n-1
{
dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] = (dp[i + 1][j][k + diff*(2*j - z - 1)][z + 1] + dp[i][j][k][z]*(2-z)*(j-z))%MOD;//没有连接到结尾
}
if(k + diff*(2*j - z + 1) <= l) //新建连通块
{
dp[i + 1][j + 1][k + diff*(2*j - z + 1)][z + 1] = (dp[i + 1][j + 1][k + diff*(2*j - z + 1)][z + 1] + dp[i][j][k][z]*(2-z))%MOD; //找一个结尾创建
}
}
//接下来填充尾部
//先创建一个新连通块
if(k + diff*(2*j - z + 2) <= l) // 2个新位置可以更新
{
dp[i + 1][j + 1][k + diff*(2*j - z + 2)][z] = (dp[i + 1][j + 1][k + diff*(2*j - z + 2)][z] + dp[i][j][k][z])%MOD;
}
//合到一个连通块中
if(k + diff*(2*j - z) <= l)
{
dp[i + 1][j][k + diff*(2*j - z)][z] = (dp[i + 1][j][k + diff*(2*j - z)][z] + dp[i][j][k][z]*(2*j - z))%MOD;
}
//然后把两个连通块合在一起
if((k + diff*(2*j - z - 2) <= l) && (j >= 2) && (i == n - 1 || j > 2 || z < 2))
{
if(z == 0)
{
dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z]*j*(j-1))%MOD; //j*P2种可能的合并
}
if(z == 1)
{
dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z]*(j-1)*(j-1))%MOD; // (j-1)P2+(j-1) 种可能的合并
}
if(z == 2)
{
if(i == n - 1)
{
dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z])%MOD;//一种可能的合并,直接继承过来
}
else
{
dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] = (dp[i + 1][j - 1][k + diff*(2*j - z - 2)][z] + dp[i][j][k][z]*(j-2)*(j-1))%MOD;// (j-2)P2 + 2(j-2)种可能的合并
}
}
}
}
}
}
}
ll answer = 0;
for(int i = 0; i <= l; i++)
{
answer = (answer + dp[n][1][i][2])%MOD;
}
cout << answer << '\n';
return 0;
}
常见的DP技巧还有很多,文文这里仅举5例,难度依次递增。
由于文文水平不足,难免存在错误或纰漏,欢迎指正。
有更多技巧想要和文文探讨,可以QQ(434935191)或邮箱(v@18sec.cn)联系文文。
[文文殿下]基本的DP技巧的更多相关文章
- 树形dp技巧,多叉树转二叉树
今天复习树形dp时发现一道比较古老的题,叫选课,是树形dp的一道基础题,也是多叉树转二叉树应用的模版题 多叉树转二叉树的应用非常广泛,因为如果一个节点的儿子太多,一个一个存下来不方便去查询,并且会增加 ...
- Codeforces 900 E. Maximum Questions (DP,技巧)
题目链接:900 E. Maximum Questions 题意: 给出一个长度为n只含有a和b还有'?'的串s,且'?'可以被任意替换为a或b.再给出一个字符串t (奇数位上为a,偶数位上为b,所以 ...
- Two Melodies CodeForces - 813D (DP,技巧)
https://codeforces.com/problemset/problem/813/D dp[i][j] = 一条链以i结尾, 另一条链以j结尾的最大值 关键要保证转移时两条链不能相交 #in ...
- 【文文殿下】WC2019游记
Day0 今天早上三点半才睡着,五点起床,前往省城郑州.与省实验常老师汇合,坐上高铁,下午三点半多才到广州二中. 下午随便找了一个教室进去敲一敲代码,发现自己越来越菜了. 和一大堆网上的dalao面基 ...
- 【文文殿下】NOIp2018游记
Day-1 本段更新于 2018年11月8日23:26:44 今天还在机房里面,无所事事吧.上午睡了一上午,出去理了一下发,花了20块钱 QAQ. 下午来到机房,复习了一下exgcd的东西. 发现自己 ...
- 【文文殿下】[CEOI2004]锯木厂选址 题解
题解 我们枚举建厂的位置,发现有个\(n^2\)的DP.随手搞个斜率优化到\(O(n)\). #include<bits/stdc++.h> using namespace std; ty ...
- 【文文殿下】【CF724C】Ray Tracing (中国剩余定理)
题解 我们考虑将棋盘扩大一倍,这样相当于取膜.然后,我们只要对x,y,的位置分类讨论,做四次crt就行.具体细节看文文代码. #include<cstdio> #include<al ...
- 【文文殿下】[BZOJ4008] [HNOI2015] 亚瑟王
题解 这是一个经典的概率DP模型 设\(f_{i,j}\)表示考虑到前\(i\)张牌,有\(j\)轮没打出牌的可能性,那么显然\(f_{0,r} = 1\). 考虑第\(i+1\)张牌,他可能在剩下的 ...
- 【文文殿下】【HAOI2008】硬币购物
题目描述 硬币购物一共有4种硬币.面值分别为c1,c2,c3,c4.某人去商店买东西,去了tot次.每次带di枚ci硬币,买si的价值的东西.请问每次有多少种付款方法. 数据规模 di,s<=1 ...
随机推荐
- netbeans php环境搭建
jdk必须: sudo apt-get install openjdk-7-jdk
- Docker简介及基本应用
Docker 前言 1.虚拟化 在计算机中,虚拟化(英语:Virtualization)是一种资源管理技术,是将计算机的各种实体资源,如服务器.网络.内存及存储等,予以抽象.转换后呈现出来,打破实体结 ...
- 01c-1: 主流长远
- 138. Copy List with Random Pointer (Graph, Map; DFS)
A linked list is given such that each node contains an additional random pointer which could point t ...
- ios7 导航栏适配
ios ui开发过程中,经常会使用到导航栏,默认的样式比较单一,所以经常需要修改导航栏的样式 ios4: - (void)drawRect:(CGRect)rect { UIImage *image ...
- Visual Studio 2010 常用快捷方式
调试快捷键 F6: 生成解决方案 Ctrl+F6: 生成当前项目 F7: 查看代码 Shift+F7: 查看窗体设计器 F5: 启动调 ...
- 8-@Pointcut( "execution(* com.ctgu.controller.AccountController.transfer(..))" ) 拦截配置问题
@pointcut()可以直接指定到某个包下的某个类的某个方法上:
- 电脑清缓存(C盘占空间)
电脑缓存目录: 1.取消文件隐藏 2.找到C:\Users\lwx351192\AppData\Local\Temp目录下的三个子文件夹local,locallow,roaming里面的文件都可删除, ...
- MVC三者关系
- Socket发送文件
.Net.cs using System; using System.Collections.Generic; using System.IO; using System.Linq; using Sy ...