Processing math: 23%

算法设计与分析-动态规划

  动态规划的设计都有着一定的模式,一般要经历以下几个步骤:

  划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
  确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
  确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
  寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。一般地,只要问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

  实际应用中可以按以下几个简化的步骤进行设计:

  (1) 分析最优解的性质,并刻画其结构特征。
  (2) 递归的定义最优解。
  (3) 以自底向上或自顶向下的记忆化方式计算出最优值。
  (4) 根据计算最优值时得到的信息,构造问题的最优解。

求解整数拆分问题

  求将正整数n无序拆分成最大数为k的拆分数,要求所有的拆分方案不重复。

  问题求解:n=5k=5,对应的拆分方案有:

  5=5
  5=4+1
  5=3+2
  5=3+1+1
  5=2+2+1
  5=2+1+1+1
  5=1+1+1+1+1

  为了防止重复计数,让拆分数保持从大到小排序,正整数5的拆分数为7

  显然将正整数n无序拆分成最大数为k的拆分数,与将整数n拆分成最多不超过k个数之和的拆分数相等。

  采用动态规划求解整数拆分问题。设dp(n,k)为将数n无序拆分为最多不超过k个数之和的拆分数:

  (1) 当n=1k=1时,显然dp(n,k)=1

  (2) 当n<k时,有dp(n,k)=dp(n,n)

  (3) 当n=k时,其拆分方案包含将正整数n无序拆分成最大数为n1的拆分方案,另一种拆分方案是将n拆分成n1,所以有dp(n,n)=dp(n,n1)+1

  (4) 当n>k时,拆分方案有两种情况,正整数n无序拆分为最大数为k1的拆分方案,此时的拆分方案数为dp(n,k1),另一种情况是,余下的数无序拆分为正好k个数之和,此时的拆分方案数为dp(nk,k),所以有:dp(n,k)=dp(n,k1)+dp(nk,k)。(方案3是方案4的特例)

  对应的递归算法如下:

1
2
3
4
5
6
7
8
9
10
11
int dpf(int n, int k)
{
if (n == 1 || k == 1)
return 1;
else if (n < k)
return dpf(n, n);
else if (n == k)
return dpf(n, k - 1) + 1;
else
return dpf(n, k - 1) + dpf(n - k, k);
}

  由于nk均为正整数,可以用一个二维数组dp来存放将正整数n无序拆分成最大数为k的拆分数,dp[n][k]即为所求结果。采用递推方式实现对应的非递归算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int dp[MAXN][MAXN];

void split(int n, int k)
{
int i, j;
for (i = 1; i <= n; i++)
for (j = 1; j <= k; j ++)
{
if (i == 1 || j == 1)
dp[i][j] = 1;
else if (i < j)
dp[i][j] = dp[i][i];
else if (i == j)
dp[i][j] = dp[i][j - 1] + 1;
else if (i > j)
dp[i][j] = dp[i][j - 1] + dp[i - j][j];
}
}

求解最大连续子序列和问题

  问题求解:对于含有n个整数的序列a,设bj1)表示前j个元素a_1 \sim a_j中的以a_j结尾的最大连续子序列和,显然,当b_{j - 1} > 0时,b_j = b_{j - 1} + a_j,当b_{j - 1} \leqslant 0时,放弃前面选取的元素,从a_j开始重新选起,b_j = a_j

  所以采用动态规划的顺序法可得到b_j的递推式为:

  b_0 = 0j = 0,边界条件。

  b_j = max\{b_{j - 1} + a_j, a_j\}1 \leqslant j \leqslant n

  序列a的最大连续子序列和等于b_j1 \leqslant j \leqslant n)中的最大者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void MaxSubSum(int a[], int b[], int n)
{
int j;
b[0] = 0;
for (j = 1; j <= n; j++)
{
if (b[j - 1] + a[j] > a[j])
b[j] = b[j - 1] + a[j];
else
b[j] = a[j];
}
}

void dispMaxSum(int a[], int b[], int n)
{
int maxj, i, j, k;
maxj = 1;
for (j = 2; j <= n; j++)
if (b[j] > b[maxj])
maxj = j;
for (k = maxk; k >= 1; k--)
if (b[k] <= 0)
break;
printf("最大连续子序列和:%d", b[maxj]);
printf("所选子序列:");
for (i = k + 1; i <= maxj; i++)
printf("%d ", a[i]);
printf("\n");
}

求解最长公共子序列问题

  字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。

  给定两个序列AB,称序列ZAB的公共子序列,是指Z同是AB的子序列。

  该问题是求两序列AB的最长公共子序列(Longest Common Subsequence,简称为LCS)。

  问题求解:考虑最长公共子序列问题如何分解成子问题,设A = (a_0, a_1, \dots , a_{m - 1})B = (b_0, b_1, \dots , b_{n - 1}),设Z = (z_0, z_1, \dots , z_{k - 1})为它们的最长公共子序列。不难证明有以下性质:

  (1) 如果a_{m - 1} = b_{n - 1},则z_{k - 1} = a_{m - 1} = b_{n - 1},且(z_0, z_1, \dots , z_{k - 2})(a_0, a_1, \dots , a_{m - 2})(b_0, b_1, \dots , b_{n - 2})的一个最长公共子序列;

  (2) 如果a_{m - 1} \neq b_{n - 1}z_{k - 1} \neq a_{m - 1},蕴涵(z_0, z_1, \dots , z_{k - 1})(a_0, a_1, \dots , a_{m - 2})(b_0, b_1, \dots , b_{n - 1})的一个最长公共子序列;

  (3) 如果a_{m - 1} \neq b_{n - 1}z_{k - 1} \neq b_{n - 1},蕴涵(z_0, z_1, \dots , z_{k - 1})(a_0, a_1, \dots , a_{m - 1})(b_0, b_1, \dots , b_{n - 2})的一个最长公共子序列。

  定义c[i][j]为序列(a_0, a_1, \dots , a_{i - 1})(b_0, b_1, \dots , b_{j - 1})的最长公共子序列的长度,采用动态规划法,每考虑一个字符a[i]b[j]都为动态规划的一个阶段(共经历约m \times n个阶段)。计算c[i][j]可递归地表述如下:

  (1) c[i][j] = 0,如果i = 0j = 0

  (2) c[i][j] = c[i - 1][j - 1] + 1,如果i, j > 0a[i - 1] = b[j - 1]

  (3) c[i][j] = max(c[i][j - 1], c[i - 1][j]),如果i, j > 0a[i - 1] \neq b[j - 1]

  从上述3种情况看到,对于c[i][j],仅当情况(2)成立时表示a[i - 1] = b[j - 1],此时a[i - 1]属于最长公共子序列的元素。设最长公共子序列长度为k,用s[0 \dots k - 1]存放最长公共子序列,求k的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s[k] = '\0';  // 先在s中添加一个串结束符
while (k > 0)
{
if (c[i][j] == c[i - 1][j])
i--;
else if (c[i][j] == c[i][j - 1])
j--;
else
{
s[--k] = a[i - 1];
i--;
j--;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int LCSLength(char* a, char* b, int c[][N])
// 求最长公共子序列的长度
{
int m = strlen(a), n = strlen(b);
int i, j;
for (i = 0; i <= m; i++)
c[i][0] = 0;
for (i = 0; i <= n; i++)
c[0][i] = 0;
for (i = 1; i < m; i++)
for (j = 1; j < n; j++)
{
if (a[i - 1] == b[j - 1])
c[i][j] = c[i - 1][j - 1] + 1;
else
{
if (c[i - 1][j] >= c[i][j - 1])
c[i][j] = c[i - 1][j];
else
c[i][j] = c[i][j - 1];
}
}
return c[m][n];
}

求解0/1背包问题

  问题求解:对于可行的背包装载,背包中物品的总重量不能超过背包的容量,最佳装载是指所装入的物品价值最高,即max\{v_1 \times x_1 + v_2 \times x_2 + \dots + v_i \times x_i\},其中1 < i < nx_i01,取1表示选取物品i

  在该问题中需要决定x_1, x_2, \dots , x_n的值。假设按i = 1, 2, \dots , n的次序来确定x_i的值,共有n次决策即n个阶段,第k阶段装入物品k,其状态变量为s_k,决策变量为x_k,状态转移方程为s_k = s_{k - 1} + w_{k - 1}x_{k - 1}

  如果置x_1 = 0,则问题转变为相对于其余物品(即物品2, 3, \dots , n),背包容量仍为W的背包问题。若置x_1 = 1,问题就变为关于最大背包容量为W - w_1的问题。

  在决策x_i时,问题处于以下两种状态:

  (1) 背包中不能装下物品i,则x_i = 0,背包不增加重量和价值,背包余下容量r不变。

  (2) 背包中可以装下物品i,则x_i = 1,背包中增加重量w_i和价值v_i,背包余下容量r = r - w_i

  假设f(i, r)表示背包剩余容量为r1 \leqslant r \leqslant W),已考虑物品1, 2, \dots, i1 \leqslant i \leqslant n)时的最优解的值,即利用最优序列由最优子序列构成的结论。采用动态规划的顺序法可得到f(i, r)的递推式为:

  f(i, r) = f(i - 1, r),当r < w_i时,物品i放不下。

  f(i, r) = max\{f(i - 1, r), f(i - 1, r - w_i) + v_i\},当r \geqslant w_i,在放入和不放入物品i之间选最优解。

  其边界条件为:

  f(i, 0) = 0,背包不能装入任何物品时,总价值为0

  f(0, r) = 0,没有任何物品可装入时,总价值为0

  这样,f(n, W)便是背包问题的最优解。

  当f值确定后,推导出x的过程十分简单,若f(i, r) \neq f(i - 1, r),此时一定有f(i, r) > f(i - 1, r),表示表示选取了物品i,对应的x_i置为1,否则没有选取物品i,对应的x_i置为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int Knap(int f[MAXN][MAXW], int w[], int v[], int W, int n)
{
int i, r;
for (i = 0; i <= n; i++)
f[i][0] = 0;
for (r = 0; r <= W; r++)
f[0][r] = 0;
for (i = 1; i <= n; i++)
for (r = 1; r <= W; r++)
if (r < w[i])
f[i][r] = f[i - 1][r];
else
if (f[i - 1][r] < f[i - 1][r - w[i]] + v[i])
f[i][r] = f[i - 1][r - w[i]] + v[i];
else
f[i][r] = f[i - 1][r];
return f[n][W];
}

int TraceBack(int f[MAXN][MAXW], int w[], int v[], int x[], int W, int n)
{
int i, r = W;
int maxw = 0;
for (i = n; i > 0; i--)
if (f[i][r] != f[i - 1][r])
{
x[i] = 1;
maxw += w[i];
r -= w[i];
}
else
x[i] = 0;
return maxw;
}

求解资源分配问题

  某公司有A, B, C三个商店,拟将新招聘的5名员工分配给这三个商店,各商店得到新员工后,每年的赢利情况如下表所示,求分配给各商店各多少员工才能使公司的赢利最大?

0 1 2 3 4 5
A 0 3 7 9 12 13
B 0 5 10 11 11 11
C 0 4 6 11 12 12

  问题求解:A, B, C商店进行决策,加上一个虚拟阶段,阶段数为K = 4

  设总员工数为S(这里S = 5),用s_k表示给第k1 \leqslant k \leqslant K)个商店分配员工时的状态变量,d[k][s_k]表示在状态s_k下分配给第k个商店的员工数,即决策变量,并有s_{k + 1} = s_k - d[k][s_k](状态转移方程)。

  用v[k][s_k]表示给第k个商店分配d[k][s_k]人后所得到的赢利值。设f_k(s_k)表示在分配到第k个商店还有未分配人数s_k的情况下,分配完所有人员时公司所能获得的最大赢利,采用动态规划递序法可以得到如下递推式:

  f(s_4) = 00 \leqslant s_4 \leqslant S

  f_k(s_k) = \underset{0 \leqslant x_k \leqslant s_k}{max}\{v[k][x_k] + f_{k + 1}(s_k - x_k)\}k = 3, 2, 1

  显然f_1(S)即为最大的赢利值,对应的d[1][S]为第1阶段最优分配的人数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int Plan(int v[][MAXS], int d[][MAXS], int K, int S)
{
int x, s, k;
int maxf;
for (s = 0; s <= S; s++)
f[K][s] = 0;
for (k = K - 1; k >= 1; k--)
for (s = 0; s <= S; s++)
{
maxf = 0;
for (x = 0; x <= s; x++)
{
if ((v[k][x] + f[k + 1][s - x]) >= maxf)
{
maxf = v[k][x] + f[k + 1][s - x];
d[k][s] = x;
}
}
f[k][s] = maxf;
}
return f[1][S];
}

void dispplan(int max, int d[][MAXS], int K, int S)
{
int k, r, s;
s = d[1][S];
r = S - s;
printf("最优资源分配方案如下:\n");
for (k = 1; k < K; k++)
{
printf(" %c商店分配%d人\n", 'A' + k - 1, s);
s = d[k + 1][r];
r = r - s;
}
printf("该分配方案的总盈利为%d万元\n", max);
}