动态规划之石子合并
石子游戏有两种:一种是博弈方面;另一种是合并规划方面的。前者需要太厉害的判断,而且要用到很多数学方面的结论,所以俺不会。这里主要探讨一下石子归并方面的问题与算法。
一. 简单的石子合并
问题类似于背包问题:给出n堆石子的数量,请你求出:由这n堆石子可以合并得到的所有重量;或求出合并成两堆之后的最小质量差是多少。
其实上一篇谈到的“数字N能否表示成若干个不相同的阶乘的和”就是这个问题的前半部分,前面是采用搜索得到所有可以组成的数量,本文下面采用动态规划来求得所有可以组成的数量。然后再解决该问题的后半部分。
(1)问题前半部分
动态规划:首先是定义好状态,这个问题是求所有可以合并后的和,那么我们就可以利用标号的方法来标记所有可以合并得到的和。数组num[n]表示读入的n堆石子数量。利用bool数组f[N],(N为这n堆石子的重量之和),状态f[i] = true 表示可以得到和为i,否则=false,表示不可以得到和为i,有:
边界条件f[0] = true ---- 尽管没实际意义,但对于DP来说却是必需的
f[i+num[j]] = f[i+num[j]] | f[i]; ---- i满足的条件是:0 <= i <= total – num[j]。方程的意思就是:总数为i的状态可以加上j组成总合为 i+j 的状态。
注意点:由于动态规划需要满足无后效性,那么如果顺推,i+num[j]的顺推必然对后面的造成影响,因此递推的方向需要改成反向,这样就不会影响了:
算法:石子合并的动态规划
过程:调用DP_Stone(n)得到所有标记好的可以组成的和
DP_Stone1 (int n) {
int i, j, total;
f[0] = true; total= 420000; // 先置可以组成和为 0,total为总数量
for (j = 0; j < n; j++) {
for (i = total-num[j]; i >= 0; i--)
if (f[i] == true)
f[i+num[j]] = true;
}
//f[0] = false; //对于前面阶乘那题,根据题目说明这一句必需要
}
(2)问题的后半部分
相关题目:http://acm.tongji.edu.cn 的1017题。顺利的得到前半部分的算法之后,后半部分就很明显了,只要在DP递推中,将total置为总数量的一半,那么可以合并得到的第一堆最大数量就是0 ~ total/2 中标号为true的最大的那个。不妨设为max,则问题的答案就是:total – 2*max,即为最小数量差。
算法:合并两堆最小数量差
过程:调用DP_Stone2( n) 得到最小数量差
DP_Stone2 (int n) {
f[0] = true; max = 0; avg = total/2;
for (j = 0; j < n; j++) {
for (i = avg-num[j]; i >= 0; i--)
if (f[i] == true) {
f[i+num[j]] = true;
if (i+num[j] > max) max = i+num[j];
}
}
return (total - 2*max);
}
二. 经典的石子合并
问题描述:在一个圆形操场的四周摆放着n堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分。对于给定n堆石子,编程计算合并成一堆的最小得分和最大得分,并输出具体合并过程。
我觉得这是一道很有难度的动态规划算法题,如果不看资料,绝对是无法正确得到状态转移方程与输出最优解的合并过程的。下面我整理了一下该问题的具体算法分析设计过程。
DP算法分析与设计:我们把每一次合并划分为阶段,当前阶段中计算出的得分和作为前一次合并的基础上定义一个能使目前得分总和最大的合并方案作为一次决策。显然,某阶段的状态给阶段的决策不受这阶段以前各段状态的影响。这种无后效性的性质符最佳原理,因此可采用动态规划算法。
DP状态表示:我们用〔i,j〕表示一个从第i堆数起,顺时针数j堆时的子序列第i堆、第i+1堆、…… (i+j-1) mod n堆}。为了输出最优解的方便,我们定义一个结构体Node,它包含两个域 c --- 得分;d --- 最优化选择时子序列1的堆数。有:f[i][j]表示一个从第i堆数起,顺时针数j堆时的子序列(i, i+1... (i+j-1)%n+1)
(1) 在该子序列的各堆石子合并成一堆的过程中,各次合并得分的总和---c
(2) 形成最佳得分和的子序列1和子序列2。由于两个子序列是相邻的,因此只需记住子序列1的堆数k即可 --- d
DP边条及状态转移方程:
(1) 边条:显然,对每一堆石子来说,仅含一堆石子的序列不存在合并有:
f[i][1].c = 0, f[i][1].d = 0 (1 <= i <= N)
(2) 方程:对子序列(i,j)最后一次合并,其得分为第i堆数起,顺时针数j堆的石子总数t。被合并的两堆石子是由子序列(i, k)和((i+k-1)%n+1, j-k)(1<=k<=j-1)经有限次合并形成的。为求出最佳合并方案中的k值,我们定义如下DP方程:
当求最大得分总和时
f[i][j].c = max{ f[i][k].c + f[x][j-k] + t | 1<=k<=j-1 }---2 <=j <=n,1<=i<=n
f[i][j].d =上面最优化选择的k,即合并成f[i][j]的子序列1的堆数。
当求最小得分总和时
f[i][j].c = min{ f[i][k].c + f[x][j-k] + t | 1<=k<=j-1 }---2 <=j <=n,1<=i<=n
f[i][j].d = 上面最优化选择的k,即合并成f[i][j]的子序列1的堆数。
其中里面其余两个未知量:x =(i + k - 1)%n + 1,即第i堆数起,顺时针数 k+1 堆的堆序号。t = sum[i][j],表示从第 i 堆起,顺时针数j堆的石子总数,而sum[i][j]可以在O(n^2)时间内预处理得到。
DP编程实现:
(1) 由于该方程形式上是二维方程,且f[i][j]与f[x][j-k]有关,其中x为(i+k-1)%n,所以如果直接采用for(i)for(j)的顺推的话,是有后效的,仔细一看,方程的第2维j是j – k,与后面无关。所以编程实现时,可以采用类似于矩阵乘法链中的实现一样,将j放在前面递推。
(2) 递归计算最优解的合并策略时,倒推进行即可,可以采用如下形式:
void Print() {
if(满足递归进行条件) {
Print(); // 先处理前面的
xxxxxxx // 再处理当前
xxxxxxx
}
}
小结:对于组合最优化求解,动态规划算法无疑是最有效的算法。然而它的难点其实就是状态的设置,只有设置好状态,才可以得到物理意义直观明显的转移方程,也才好得到最优化策略的应用。
附录:--- 经典石子合并源程序
#include <stdio.h>
#include <string.h>
#define N 500
#define oo 2000000000
#define MIN(a, b) (a)<(b)?(a):(b)
#define MAX(a, b) (a)>(b)?(a):(b)
typedef struct { int c, d; } Node;
int n;
int v[N]; // 每堆石头的个数
int save[N]; // 输出最优解的具体合并需要随时改变 v 的值,所以为了同时输出最小,最大的合并,在完成一个任务之后需要回溯
Node f[N][N]; // f[i][j]存储最优解,同时存储合并线索
int sum[N][N]; // sum[i][j] 表示从第 i 堆起,顺时针数j堆的石子总数
void Print(int i, int j) // 递归打印子序列f[i][j]的合并过程
{
int k, x;
if(j != 1) {
Print(i, f[i][j].d); // 倒推子序列 1 的合并过程
x = (i + f[i][j].d - 1)%n + 1;
Print(x, j - f[i][j].d); // 倒推子序列 2 的合并过程
for(k = 1; k <= n; k++) // 输出当前合并第i堆和第x堆的方案
if(v[k] > 0)
{
if(i == k || x == k) printf("-%d ", v[k]); // -号表示这次操作合并该堆
else printf("%d ", v[k]);
}
printf("/n");
v[i] = v[i] + v[x]; // 新堆的大小
v[x] = -v[x]; // 置为"-" 类似于删除
}
}
void Solve(int flag) // flag = 0求最小得分, flag = 1 求最大得分
{
int i, j, k, x, t, result;
for(i = 1; i <= n; i++) // 仅含一堆石子的序列不存在合并
f[i][1].c = f[i][1].d = 0;
for(j = 2; j <= n; j++) { // 顺推含2堆,3堆...n堆石子的各子序列的合并方案
for(i = 1; i <= n; i++) {
t = sum[i][j];
if(flag == 0) f[i][j].c = oo; // 求最小得分,那么需要初始化为 oo
else f[i][j].c = 0; // 求最大得分,那么需要初始化为 0
for(k = 1; k <= j-1; k++) {
x = (i + k - 1)%n + 1;
if((flag == 0 && f[i][k].c + f[x][j-k].c + t < f[i][j].c)
||(flag != 0 && f[i][k].c + f[x][j-k].c + t > f[i][j].c)) {
f[i][j].c = f[i][k].c + f[x][j-k].c + t;
f[i][j].d = k;
}
}
}
}
result = f[1][n].c; k = 1; // 在子序列f[1][n], f[2][n]...f[n][n]中寻找最优值
for(i = 2; i <= n; i++)
if((flag == 0 && f[i][n].c < result) || (flag != 0 && f[i][n].c > result))
{
result = f[i][n].c;
k = i; // 记录下 k
}
printf("%s score is : %d/n", flag == 0 ? "min" : "max", result);
printf("合并过程如下:/n");
Print(k, n); // 由此 k 出发 倒推合并过程
printf("%d/n/n", sum[1][n]); // 输出最后一次将石子合并成一堆的石子总数
}
int main()
{
int i, r, j, k, x, t;
while(scanf("%d", &n), n) {
for(i = 1; i <= n; i++) scanf("%d", &v[i]);
//memset(sum, 0, sizeof(sum));
memcpy(save+1, v+1, n*sizeof(v[1]));
for(i = 1; i <= n; i++) sum[i][1] = v[i];
for(j = 2; j <= n; j++)
for(i = 1; i <= n; i++)
sum[i][j] = v[i] + sum[i%n+1][j-1];
Solve(0);
memcpy(v+1, save+1, n*sizeof(v[1])); // 回溯 v以便求最大得分
Solve(1);
}
return 0;
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/hqd_acm/archive/2010/02/20/5311762.aspx