zoukankan      html  css  js  c++  java
  • [文文殿下]基本的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(nsqrt{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 << '
    ';
    	return 0;
    }
    

    常见的DP技巧还有很多,文文这里仅举5例,难度依次递增。

    由于文文水平不足,难免存在错误或纰漏,欢迎指正。

    有更多技巧想要和文文探讨,可以QQ(434935191)或邮箱(v@18sec.cn)联系文文。

  • 相关阅读:
    C#利用反射动态调用类及方法
    系统程序监控软件
    SQL server 2008 安装和远程访问的问题
    sql server 创建临时表
    IIS 时间问题
    windows 2008 安装 sql server 2008
    sql server xml nodes 的使用
    Window 7sp1 安装vs2010 sp1 打开xaml文件崩溃
    CSS资源网址
    Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, Version=3.0.0.0
  • 原文地址:https://www.cnblogs.com/Syameimaru/p/9692137.html
Copyright © 2011-2022 走看看