动态规划「非典型」

动态规划「非典型」

我们通过「典型」的动态规划题学习理解动态规划,但我们还需要解决其他「非典型」的动态规划夯实 DP 技巧。本篇文章将讨论两道「非典型」动态规划题,之前 UVa11450 Wedding Shopping 已经在这篇文章中讲解过了,如有需要回顾的,请点击这里:动态规划「入门」

1. UVa 10943 How do you add?

problem

一个正整数 n n n,关于 k k k 个数字相加等于 n n n 的方法共有多少种?

constraints

1 ≤ n , k ≤ 100 1\leq n, k\leq 100 1n,k100

sample input

20 2

sample output

21

sample explain

0+20
1+19
2+18
3+17
4+16
5+15

18+2
19+1
20+0

解析

首先,根据问题有几个参数来分析决定问题的状态。问题只有两个参数, n n n k k k。那么将有 4 4 4 种可能:

  • 如果不选择任何参数,无法表示状态
  • 如果只选择了 n n n,那么无法得知多少个数字已经选择了
  • 如果只选择了 k k k,那么无法得知目标之和 n n n
  • 因此,该问题的状态应该为一对 ( n , k ) (n,k) (n,k),至于选择的数字顺序无关紧要。

接下来,观察基础情况,看起来 k = 1 k=1 k=1 的时候非常简单,无论 n n n 是多少,只有一种方法,选择一个和 n n n 一样的数字。

对于其余情况,状态 ( n , k ) , k > 1 (n, k), k\gt 1 (n,k),k>1,我们可以将 n n n 拆分为两个数字 x x x n − x n-x nx,也就是 n = x + ( n − x ) n=x + (n-x) n=x+(nx)。这样做我们得到了子问题(子状态) ( n − x , k − 1 ) (n - x, k - 1) (nx,k1),即一个整数 n − x n-x nx,关于 k − 1 k - 1 k1 个数字相加等于 n − x n-x nx 的方法共有多少种?我们可以通过以下方法得到结果:

  1. w a y s ( n , 1 ) = 1 ; ways(n, 1) = 1; ways(n,1)=1; 我们只可以使用一个 n n n 数字本身,相加等于 n n n
  2. w a y s ( n , k ) = ∑ x = 0 n w a y s ( n − x , k − 1 ) ; ways(n, k) = \sum_{x=0}^{n}ways(n-x, k-1); ways(n,k)=x=0nways(nx,k1); 递归地求出所有方案数量

该问题具有重叠子结构,状态共有 n × k n\times k n×k 个,计算每个状态所需 O ( n ) O(n) O(n),因此时间复杂度是 O ( n 2 k ) O(n^2k) O(n2k)。需要注意该问题需要对 1000000 1 000 000 1000000 取模结果。

code
#include <bits/stdc++.h>
using namespace std;

int main() {
	int n, k;
	while (cin >> n >> k, n + k != 0) {
		// dp[i][j] 第 i 个数字构成 j 之和的方案数量
		vector<vector<int>> dp(k + 1, vector<int>(n + 1));
		
		// 当只有 1 个数字时,方案数量只有 1 种
		for (int i = 0; i <= n; i++) {
			dp[1][i] = 1;
		}
		
		// 在第 i 个数字
		for (int i = 2; i <= k; i++) {
			// 不同 j 之和的状态下,这里 j = 0 需要取到,或者可以在初始化的时候将所有列为 0 设置为 1
			for (int j = 0; j <= n; j++) {
				int sum{0};
				// 可以选择的数字 x 有 0 ~ j
				for (int x = 0; x <= j; x++) {
					sum = (sum + dp[i - 1][j - x]) % 1000000;
				}
				dp[i][j] = sum;
			}
		}
		
		cout << dp[k][n] << "\n";
	}
    return 0;
}

2. UVa 10003 Cutting Sticks

problem

给定一段长 l ( 1 ≤ l ≤ 1000 ) l(1\leq l\leq 1000) l(1l1000) 的木块和 n ( 1 ≤ n ≤ 50 ) n(1\leq n\leq 50) n(1n50) 处切割点,范围在 [ 0 , l ] [0, l] [0,l] 之间。每次切割的花费是切割下的两端木块长度之和。你的任务是找出一种切割顺序使得总花费最小。

sample input

100
3
25 50 75


10
4
4 5 7 8

sample output

The minimum cutting is 200.
The minimum cutting is 22.

smaple explain

对于第一个样例,可以按照以下顺序切割木块。

  1. 第一次切割 50 50 50,花费 s u m = ( 50 + 50 ) = 100 sum = (50 + 50) = 100 sum=(50+50)=100
  2. 第二次切割 25 25 25,花费 s u m = 100 + ( 25 + 25 ) = 150 sum = 100 + (25 + 25) = 150 sum=100+(25+25)=150
  3. 第三次切割 75 75 75,花费 s u m = 150 + ( 25 + 25 ) = 200 sum = 150 + (25 +25) = 200 sum=150+(25+25)=200

解析

使用端点来表示每根木块。通过引入两个额外的坐标值,即 0 0 0 l l l,将木块的端点描述为 {0,原始切割端点,l},状态为需要切割木块 cut(left, right);,表示木块从 left 端至 right 端需要切割:

  • 如果 left+1 = right,即木块段不需要进一步切割,则 cut(i-1, i) = 0,第 i − 1 i-1 i1 端点和第 i i i 端的切割费用为 0 0 0
  • 否则,通过尝试所有可能的切割点来找到最佳切割方案,得到 cut(left, right) = min(cut(left, i) + cut(i, right) + (A[right]-A[left])) ∀ i ∈ [ l e f t + 1 , r i g h t − 1 ] \forall i\in[left + 1, right -1] i[left+1,right1]

可以通过计算 cut(0, n+1) 得到。

在这里存在重叠子问题,实际搜索空间并不是很大。所有可能的左/右索引数为 O ( n 2 ) O(n^2) O(n2),并且可以进行记忆化。由于每个状态的计算时间为 O ( n ) O(n) O(n),因此自顶向下的动态规划总时间复杂度为 O ( n 3 ) O(n^3) O(n3)

综上所述,该算法是可行的。

code
#include <bits/stdc++.h>
using namespace std;

const int INF = 0x3f3f3f3f;

int wood, n, sec;
vector<int> w;
vector<vector<int>> memo;

int dp(int L, int R) {
	if (L + 1 == R) return 0;
	
	if (memo[L][R] != -1) return memo[L][R];
	
	int ans = INF;
	for (int i = L + 1; i < R; i++) {
		ans = min(ans, dp(L, i) + dp(i, R) + (w[R] - w[L]));
	}
	return memo[L][R] = ans;
}

int main() {
	while (cin >> wood, wood) {
		cin >> n;
		
		w.assign(n + 2, 0);
		for(int i = 1; i <= n; i++) {
			cin >> w[i];
		}
		w[0] = 0;
		w[n + 1] = wood;
		
		memo.assign(n + 2, vector<int>(n + 2, -1));
		cout << "The minimum cutting is " << dp(0, n + 1) << ".\n";
	}
    return 0;
}

转变为自底向上动态规划。

#include <bits/stdc++.h>
using namespace std;

const int INF = 0x3f3f3f3f;

int main() {
    int wood, n, sec;
    while (cin >> wood, wood) {
        cin >> n;
        
        vector<int> w(n + 2);
       	// dp[i][j] 表示第 i 个切割点到第 j 个切割点构成的木块花费最小值
        vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
        
        for(int i = 1; i <= n; i++) {
            cin >> w[i];
        }
        w[0] = 0;
        w[n + 1] = wood;
        
        // 自底向上的动态规划
        // 枚举 [L, R] 的所有状态,对于长度为 1 忽略 默认设置为了花费 0
        for (int len = 2; len <= n + 1; len++) {
            for (int L = 0; L + len <= n + 1; L++) {
                int R = L + len;
                dp[L][R] = INF;
                for (int i = L + 1; i < R; i++) {
                    dp[L][R] = min(dp[L][R], dp[L][i] + dp[i][R] + (w[R] - w[L]));
                }
            }
        }
        
        cout << "The minimum cutting is " << dp[0][n + 1] << ".\n";
    }
    return 0;
}
  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值