.

二进制状态压缩动态规划

对于某些情况,如果题目中所给的限制数目比较小,我们可以尝试状态压缩动态规划。例如,题目中给出数据范围\(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技巧的更多相关文章

  1. 树形dp技巧,多叉树转二叉树

    今天复习树形dp时发现一道比较古老的题,叫选课,是树形dp的一道基础题,也是多叉树转二叉树应用的模版题 多叉树转二叉树的应用非常广泛,因为如果一个节点的儿子太多,一个一个存下来不方便去查询,并且会增加 ...

  2. Codeforces 900 E. Maximum Questions (DP,技巧)

    题目链接:900 E. Maximum Questions 题意: 给出一个长度为n只含有a和b还有'?'的串s,且'?'可以被任意替换为a或b.再给出一个字符串t (奇数位上为a,偶数位上为b,所以 ...

  3. Two Melodies CodeForces - 813D (DP,技巧)

    https://codeforces.com/problemset/problem/813/D dp[i][j] = 一条链以i结尾, 另一条链以j结尾的最大值 关键要保证转移时两条链不能相交 #in ...

  4. 【文文殿下】WC2019游记

    Day0 今天早上三点半才睡着,五点起床,前往省城郑州.与省实验常老师汇合,坐上高铁,下午三点半多才到广州二中. 下午随便找了一个教室进去敲一敲代码,发现自己越来越菜了. 和一大堆网上的dalao面基 ...

  5. 【文文殿下】NOIp2018游记

    Day-1 本段更新于 2018年11月8日23:26:44 今天还在机房里面,无所事事吧.上午睡了一上午,出去理了一下发,花了20块钱 QAQ. 下午来到机房,复习了一下exgcd的东西. 发现自己 ...

  6. 【文文殿下】[CEOI2004]锯木厂选址 题解

    题解 我们枚举建厂的位置,发现有个\(n^2\)的DP.随手搞个斜率优化到\(O(n)\). #include<bits/stdc++.h> using namespace std; ty ...

  7. 【文文殿下】【CF724C】Ray Tracing (中国剩余定理)

    题解 我们考虑将棋盘扩大一倍,这样相当于取膜.然后,我们只要对x,y,的位置分类讨论,做四次crt就行.具体细节看文文代码. #include<cstdio> #include<al ...

  8. 【文文殿下】[BZOJ4008] [HNOI2015] 亚瑟王

    题解 这是一个经典的概率DP模型 设\(f_{i,j}\)表示考虑到前\(i\)张牌,有\(j\)轮没打出牌的可能性,那么显然\(f_{0,r} = 1\). 考虑第\(i+1\)张牌,他可能在剩下的 ...

  9. 【文文殿下】【HAOI2008】硬币购物

    题目描述 硬币购物一共有4种硬币.面值分别为c1,c2,c3,c4.某人去商店买东西,去了tot次.每次带di枚ci硬币,买si的价值的东西.请问每次有多少种付款方法. 数据规模 di,s<=1 ...

随机推荐

  1. 安装Arch Linux(桌面环境)

    安装xorg-server # pacman -S xorg-server xorg-server-utils xorg-xinit 安装显卡驱动 如果不知道是什么显卡,就使用以下命令查看 # lsp ...

  2. Windows 服务 创建 和 安装 -摘自网络

    What a Windows Service is Enables you to create long-running executable applications that run in the ...

  3. Ext.grid.panel 改变某一行的字体颜色

    grid.getStore().addListener('load', handleGridLoadEvent); function handleGridLoadEvent(store, record ...

  4. Python字符串 --Python3

    Python语言中,字符串是用两个双引号或者单引号括起来的词汇表或多个字符. 1.Python字符串的两种序号体系 反向递减序号:-- -9 -8 -7 -6 -5 -4 -3 -2 -1 正向递增序 ...

  5. JAVA复习笔记分布式篇:zookeeper

        前言:终于到分布式篇,前面把JAVA的一些核心知识复习了一遍,也是一个JAVA程序员最基本要掌握的知识点,接下来分布式的知识点算是互联网行业的JAVA程序员必备的技能:     概念:ZooK ...

  6. ZH奶酪:JavaScript中的JSON.stringify() and JSON.parse()

    JSON.stringify() JSON.stringify()可以将任意的JavaScript值序列化成JSON字符串. 语法 JSON.stringify(value[, replacer [, ...

  7. Linux内核源代码分析方法

    Linux内核源代码分析方法   一.内核源代码之我见 Linux内核代码的庞大令不少人"望而生畏",也正由于如此,使得人们对Linux的了解仅处于泛泛的层次.假设想透析Linux ...

  8. Spring整合Mybatis解决 Property &#39;sqlSessionFactory&#39; or &#39;sqlSessionTemplate&#39; are required

    在Spring4和Mybatis3整合的时候,dao层注入'sqlSessionFactory'或'sqlSessionTemplate'会报错解决办法如下: package com.alibaba. ...

  9. android课程第一节(TextView控件使用)

    TextView控件使用 一.TextView基本使用(创建方式) 1.在程序中创建TextView对象 如下代码: @Override protected void onCreate(Bundle ...

  10. OPENGL_单位长度对应屏幕像素

    gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar) fovy:视角,指定视景体的视野的角度,以度数为单 ...