动态规划问题(三)

前篇传送门:

动态规划问题(一)_Wmiracle的博客-CSDN博客

动态规划问题(二)_Wmiracle的博客-CSDN博客

七、区间DP

有 n 堆石子排成一排,每堆石子有一定的数量,将 n 堆石子合并成一堆。合并的规则是每次只能合并相邻的两堆石子,合并的花费为这两堆石子的总数。石子经过 n - 1 次合并后成为一堆,求总的最小花费。

以最小花费为例,设dp[i][j]表示合并第 i 堆石子到第 j 堆石子的最小花费,sum[i][j]为从第 i 到 j 的区间的石子数和(即花费和)。

我们从最简单的合并——两堆合并开始分析:合并第 i 堆和第 i + 1 堆的花费为第 i 堆的花费 + 第 i + 1 堆的花费,考虑到第 i 堆和第 i + 1 堆可能分别已经发生过合并,因此可以得到dp[i][i + 1] = dp[i][i] + dp[i + 1][i + 1] + sum[i][i + 1]

再分析三堆合并:合并第 i 堆、第 i + 1 堆和第 i + 2堆的花费有两种情况,

一种是先合并第 i 堆和第 i + 1堆,再和第 i + 2堆合并,这种情况下dp[i][i + 2] = dp[i][i + 1] + dp[i + 2][i + 2] + sum[i][i + 2]

另一种是先合并第 i + 1堆和第 i + 2堆,再和第 i 堆合并,这种情况下dp[i][i + 2] = dp[i][i] + dp[i + 1][i + 2] + sum[i][i + 2]

二者的最小值即为最小花费,即dp[i][i + 2] = min(dp[i][i] + dp[i + 1][i + 2], dp[i][i + 1] + dp[i + 2][i + 2]) + sum[i][i + 2]

再推广到第 i 堆到第 j 堆的合并,可以进一步化为第 i 到第 k 堆的合并 + 第 k + 1 到第 j 堆的合并,因此状态转移方程为

dp[i][j] = \begin{cases} 0, & i = j \\ min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[i][j - i + 1]), & i \neq j, \; \; i \leqslant k \leqslant j \end{cases}

C++未优化部分代码如下:

for(int i = 1;i <= n; i++)
    dp[i][i] = 0;
for(int len = 1; len < n; len++){
    for(int i = 1; i <= n - len; i++){
        int j = i + len;
        dp[i][j] = INF;
        for(int k = i; k < j; k++)
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
    }
}

很明显时间复杂度为O(n^3),只适用于小数据的情况。当数据较大时,我们可以使用平行四边形优化的方法。用s[i][j]表示从第 i 堆石子到第 j 堆石子的最优分割点,由平行四边形优化原理,可以得到s[i][j - 1] \leqslant s[i][j] \leqslant s[i + 1][j],那么 k 只需枚举s[i][j - 1]s[i + 1][j],每次记录最优分割点即可。

C++优化部分代码如下:

for(int i = 1;i <= n; i++){
    dp[i][i] = 0;
    s[i][i] = i;
}
for(int len = 1; len < n; len++){
    for(int i = 1; i <= n - len; i++){
        int j = i + len;
        dp[i][j] = INF;
        for(int k = s[i][j - 1]; k <= s[i + 1][j]; k++)
            if(dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1] < dp[i][j]){
                dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
                s[i][j] = k;
            }
    }
}

例题:AcWing282 石子合并 

八、树形DP 

某大学有 n 个职员,编号为 1...n。

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r_i​,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

我们令dp[i][0]表示不选择当前结点的最优解,dp[i][1]表示选择当前结点的最优解,可以分成两种情况:

第一种是不选择当前结点,那么子结点的选择没有限制,即既可选择子结点,又可不选择子结点,因此只要取两者的最大值,即dp[u][0] += max(dp[son][1], dp[son][0])

第二种是选择当前结点,那么子结点只能不选择,即dp[u][1] += dp[son][0]

综上分析,状态转移方程为ans = max(dp[t][1], dp[t][0]),其中t为根结点。

C++代码如下:

#include 
#include 
#include 
#include 

using namespace std;

const int N = 6000 + 5;
int value[N], dp[N][2], father[N];   // value[i]存储结点i的权值,father[i]存储结点i的父结点
vector tree[N];

void dfs(int u){
    dp[u][0] = 0;   // u不参加宴会的初值
    dp[u][1] = value[u];   // u参加宴会的初值
    for(int i = 0; i < tree[u].size(); i++){   // 处理u的子结点
        int son = tree[u][i];
        dfs(son);
        dp[u][0] += max(dp[son][1], dp[son][0]);   // 第一种情况
        dp[u][1] += dp[son][0];   // 第二种情况
    }
}

int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++){
        cin >> value[i];
        father[i] = -1;   //赋初值
    }
    for(int i = 1; i <= n - 1; i++){
        int a, b;
        cin >> a >> b;
        tree[b].push_back(a);   // 邻接表建树
        father[a] = b;   // 建立父子关系
    }
    int t = 1;
    while(father[t] != -1){   // 查找根结点
        t = father[t];
    }
    dfs(t);   // dfs遍历整棵树
    cout << max(dp[t][1], dp[t][0]) << endl;
    return 0;
}

例题:luoguP1352 没有上司的舞会

九、数位DP

一个数字,如果包含'4'或者'62',它是不吉利的。给定m和n,统计[m, n]范围内吉利数的个数。

可以从高位到低位的顺序进行排除不符合条件的数,例如范围给定[1, 999999],先排除最高位是4的数和最高位是6,次高位是2的数,即400000~499999、620000~629999;在剩下的数中再排除次高位是4的数和次高位是6,次次高位是2的数;以此类推直到结束。实现这个思路有两种方法,一种是递推,另一种是记忆化搜索。

先来看递推实现数位DP:

dp[i][j]表示 i 位数中首位是 j 符合要求的数的个数。非常容易得到状态转移方程为

dp[i][j] = \begin{cases} 0, & i = 0 \\ \sum_{k = 0}^{9}dp[i - 1][k], & i \neq 0, \; \; j \neq 4 \; \; and \; \; (j \neq 6 \; \; and \; \; k \neq 2) \end{cases}

C++代码如下:

#include 
#include 
#include 

using namespace std;

const int LEN = 7 + 1;
int dp[LEN][10];
int digit[LEN];   // digit[i]存储第i位数字

void init(){   // 预处理计算dp[][]
	dp[0][0] = 1;
	for(int i = 1; i < LEN; i++)
		for(int j = 0; j < 10; j++){
			if(j != 4)   // 排除数字4
				for(int k = 0; k < 10; k++){
					if(j == 6 && k == 2) continue;   // 排除数字62
					dp[i][j] += dp[i - 1][k];
				}
		}
}

int solve(int n){
	int len = 0;   // len表示n的位数
    while(n){
    	digit[++len] = n % 10;
   		n /= 10;
	}
	digit[len + 1] = 0;
	int ans = 0;
	for(int i = len; i > 0; i--){   // 从高位到低位处理
		for(int j = 0; j < digit[i]; j++){
			if(j == 2 && digit[i + 1] == 6) continue;
			ans += dp[i][j];
		}
		if(digit[i] == 4 || digit[i] == 2 && digit[i + 1] == 6) break;   // 第i位是4或者第i+1位和第i位是62不符合条件
	}
	return ans;
}

int main()
{
    int n, m;
    init();
    while(cin >> n >> m && (n | m)){
		cout << solve(m + 1) - solve(n) << endl;
	}
	return 0;
}

再看看用记忆化搜素的方法实现数位DP:

dp[i][0 / 1]表示不含4和62的前提下首位是否为6的数的个数(1表示是,2表示否),记忆化搜索的本质在于用dfs遍历到最深处(本题以数位长度不断减少1来遍历,最深处为数位长度等于0),然后逐步回退,将每次计算结果保存下来,在再次遇见相同的计算时可以直接使用。

C++代码如下:

#include 
#include 
#include 
 
using namespace std;

const int LEN = 7 + 1;
int dp[LEN][2];   // dp[i][0 / 1]表示不含4和62的前提下首位是否为6的个数(1表示是,0表示否)
int digit[LEN];

int dfs(int len, bool state, bool fp){   // state表示dp的状态,即不含4和62的前提下首位是否为6
	if(!len) return 1;   // 已经递归到0位数,返回
	if(!fp && dp[len][state] != -1) return dp[len][state];   // 如果已经算过则直接使用
	int res = 0, fpmax = fp ? digit[len] : 9;
	for(int i = 0; i <= fpmax; i++){
		if(i == 4 || state && i == 2) continue;   // 排除出现4和62的情况
		res += dfs(len - 1, i == 6, fp && i == fpmax);
	}
	if(!fp) dp[len][state] = res;
	return res; 
}

int solve(int n){
	int len = 0;
	while(n){
		digit[++len] = n % 10;
		n /= 10;
	}
	return dfs(len, false, true);
}

int main()
{
    int n, m;
    memset(dp, -1, sizeof dp);   // 初始化为-1
    while(cin >> n >> m && (n | m)){
    	cout << solve(m) - solve(n - 1) << endl;
	}
	return 0;
}

例题:hdu2089 不要62

十、状压DP

农夫约翰有一片长方形土地,划成 M 行 N 列的方格。他准备种玉米、养牛。

遗憾的是,有些田地很贫瘠,不能种玉米。

而且,牛不喜欢彼此靠近吃东西,所以牛不能放在相邻的格子里。

给出这块地的情况,求约翰有多少种种玉米的方案。

所有方格不种玉米也算一种方案。

既然是个方格图,首先得对这些方格进行表示,由于方格上只有两种状态:种玉米和不种玉米,因此容易想到可以用二进制来表示,用1表示种玉米,0表示不种玉米。以每一行为一个二进制序列,每种二进制序列用一个编号表示,如1表示000,2表示001,3表示010,4表示100(排除了相邻情况)……。我们可以令dp[i][j]表示第 i 行采用第 j 种编号的方案时前 i 行可以得到的可行方案总数。

从第 i - 1 行转移到第 i 行,第 i 行取每种方案的可行方案总数为dp[i][k] = \sum_{j = 1}^{n}dp[i - 1][j],其中得保证所有的dp[i - 1][j]与第 i 行不冲突。

最后把最后一行的dp[m][k]\;(1 \leqslant k \leqslant n)相加即为答案。

注意:相邻两行是否有挨着的1可以这样判断:

if(state[i] & state[j]) {...}   // 相邻两行有挨着的1
if(!(state[i] & state[j])) {...}   // 相邻两行没有挨着的1

C++代码如下:

#include 
#include 
#include 
 
using namespace std;

typedef long long ll;
const int mod = 1000000000;
const int N = 1 << (12 + 5);
int n, m, cnt;
int state[N], initst[N];   // state[i]存储一行中第i种可行状态,initst[]存储原状态
int dp[12 + 5][N];

bool check(int state){   // 判断是否有相邻的
	if(state & (state << 1)) return false;   // 有相邻的1,不合法
	return true;   // 没有相邻的1,合法
}

int solve(int n, int m)
{
    for(int i = 0; i < (1 << m); i++)   // 初始化合法方案
    	if(check(i)) state[cnt++] = i;   // 记录合法方案
    
	for(int i = 0; i < n; i++)
		for(int j = 0; j < m; j++){
			int x;
			cin >> x;
			if(x == 0) initst[i] |= (1 << j);
		}
    
	for(int i = 0; i < cnt; i++)
		if(!(initst[0] & state[i])) dp[0][i] = 1;   // 将可行状态且不与第一行发生冲突的初始化为1
	
    for(int i = 1; i < n; i++)
		for(int j = 0; j < cnt; j++){
			if(initst[i] & state[j]) continue;   // 有挨着的1,不合法
			for(int k = 0; k < cnt; k++){
				if(initst[i - 1] & state[k] || state[j] & state[k]) continue;   // 有挨着的1,不合法
				dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;
			}
		}
	int res = 0;
	for(int i = 0; i < cnt; i++)
		res = (res + dp[n - 1][i]) % mod;
    return res;
}

int main()
{
    cin >> n >> m;
	cout << solve(n, m) << endl;
	return 0;
}

例题:poj3254 Corn Fields(玉米田)

           luoguP2704 炮兵阵地

你可能感兴趣的:(算法基础,动态规划,算法,c++)