动态规划经典题——石子合并

石子合并

  • 题目信息
    • 路边玩法
    • 操场玩法
  • 问题分析
    • 路边玩法
    • 操场玩法
  • 算法设计
    • 路边玩法
    • 操场玩法
  • 完美图解(以路边玩法为例)
  • 伪代码详解
    • 路边玩法
    • 操场玩法
  • 实战演练
    • 优化后代码
  • 1274:【例9.18】合并石子

题目信息

路边玩法

有n堆石子放在路边一列,现在要将石子有序地合并成一堆,规定每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费(最小或最大)

操场玩法

一个圆形操场周围摆放着n堆石子圆圈,现在要将石子有序地合并成一堆,规定每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费(最小或最大)

问题分析

路边玩法

如果n-1次合并的全局最优解包含了每一次合并的子问题的最优解,那么经这样的n-1次合并后的花费总和必然是最优的。
动态规划经典题——石子合并_第1张图片

操场玩法

如果把路边玩法看成石子合并问题,那么操场玩法就属于圆形石子合并问题。圆形石子合并经常转换为直线型来求。也就是说,把圆形结构看成是长度为原规模两倍的直线结构来处理,如果操场玩法的规模为n,所以相当于有一排石子 a1, a2, … an, a1, a2, …, an-1,该问题规模为2n-1。最后从规模是n的最优值找出最小值或最大值即可

算法设计

路边玩法

1. 确定合适的数据结构
	采用一维数组 a[i]来记录第i堆石子的数量;
	sum[i]来记录前i堆石子的总数量;
	二维数组Min[i][j]、Max[i][j]来记录第i堆到第j堆石合并的最小和最大花费
2. 初始化
	输入石子的堆数n,然后依次输入各堆石子的数量存储在a[i]中,
	令Min[i][j]=0,Max[i][j]=0,sum[0]=0,计算sum[i].
3.循环阶段
	按照递归式计算2堆石子合并{ai, a i+1}的最小和最大花费
	依次类推,直到求出所有堆的最小和最大花费
4.构造最优解
	Min[1][n]和Max[1][n]是n堆石子合并的最小和最大花费。
	如果还想知道具体的合并顺序,需要在求解的过程中记录最优决策,然后逆向构造最优解,
可以使用类似矩阵连乘的构造方法,用括号来表达合并的先后顺序。	

操场玩法

从规模为n的最优值M[1][n],Min[2][n+1],Min[3][n+2],,,Min[n][2n-1]
中找最小值作为圆形石子合并的最小花费。
最大值同理

完美图解(以路边玩法为例)

动态规划经典题——石子合并_第2张图片动态规划经典题——石子合并_第3张图片动态规划经典题——石子合并_第4张图片剩下的可以看趣学算法

伪代码详解

路边玩法

void straight(int a[], int n) {
	for (int i = 1; i <= n; i++) {//初始化
		Min[i][i] = 0, Max[i][i] = 0;
	}
	sum[0] = 0;
	for (int i = 1; i <= n; i++) {
		sum[i] = sum[i - 1] + a[i];
	}
	for (int v = 2; v <= n; v++) {//枚举合并的堆数规模
		for (int i = 1; i <= n - v + 1; i++) {//枚举起点
			int j = i + v - 1;	//枚举终点j
			Min[i][j] = INF;		//初始化最大值
			Max[i][j] = -1;			//初始化最小值
			int tmp = sum[j] - sum[i - 1];	//记录i...j之间的石子数之和
			for (int k = i; k < j; k++) {
				Min[i][j] = min(Min[i][j], Min[i][k] + Min[k + 1][j] + tmp);
				Max[i][j] = max(Max[i][j], Max[i][k] + Max[k + 1][j] + tmp);
			}
		}
	
	}
}

操场玩法


void Circular(int a[], int n) {
	for (int i = 1; i <= n-1; i++) {
		a[n + i] = a[i];	//扩大规模
	}
	n = 2 * n - 1;
	straight(a, n);	//以2n-1的规模进行直线的运算
	n = (n + 1) / 2;	//最后只需要规模为N的最优解
	minn = Min[1][n];
	maxx = Max[1][n];
	for (int i = 2; i <= n; i++) {
		if (Min[i][n + i - 1] < minn) {
			minn = Min[i][n + i - 1];
		}
		if (Max[i][n + i - 1] > maxx) {
			maxx = Max[i][n + i - 1];
		}

	}

}

实战演练

#include
using namespace std;
const int INF = 100000;
const int N = 205;
int Min[N][N], Max[N][N];//求最小、最大值
int sum[N];//计算前i堆石子的数量总和,sum[0]=0,w(i,j)=sum[j]-sum[i-1]
int a[N];//记录各堆石子的数量
int minn, maxx;

void straight(int a[], int n) {
	for (int i = 1; i <= n; i++) {//初始化
		Min[i][i] = 0, Max[i][i] = 0;
	}
	sum[0] = 0;
	for (int i = 1; i <= n; i++) {
		sum[i] = sum[i - 1] + a[i];
	}
	for (int v = 2; v <= n; v++) {//枚举合并的堆数规模
		for (int i = 1; i <= n - v + 1; i++) {
			int j = i + v - 1;
			Min[i][j] = INF;
			Max[i][j] = -1;
			int tmp = sum[j] - sum[i - 1];
			for (int k = i; k < j; k++) {
				Min[i][j] = min(Min[i][j], Min[i][k] + Min[k + 1][j] + tmp);
				Max[i][j] = max(Max[i][j], Max[i][k] + Max[k + 1][j] + tmp);
			}
		}
	
	}
}

void Circular(int a[], int n) {
	for (int i = 1; i <= n-1; i++) {
		a[n + i] = a[i];
	}
	n = 2 * n - 1;
	straight(a, n);
	n = (n + 1) / 2;
	minn = Min[1][n];
	maxx = Max[1][n];
	for (int i = 2; i <= n; i++) {
		if (Min[i][n + i - 1] < minn) {
			minn = Min[i][n + i - 1];
		}
		if (Max[i][n + i - 1] > maxx) {
			maxx = Max[i][n + i - 1];
		}

	}

}

int main()
{
	int n;
	cout << "请输入石子的堆数 n:";
	cin >> n;
	cout << "请依次输入各堆的石子数:";
	for (int i = 1; i <= n; i++){
		cin >> a[i];
	}
	straight(a, n);
	cout << "路边玩法(直线型)最小花费为:" << Min[1][n] << endl;
	cout << "路边玩法(直线型)最大花费为:" << Max[1][n] << endl;
	Circular(a, n);
	cout << "操场玩法(圆型)最小花费为:" << minn << endl;
	cout << "操场玩法(圆型)最大花费为:" << maxx << endl;
	return 0;
}

/*
请输入石子的堆数 n:6
请依次输入各堆的石子数:5 8 6 9 2 3
路边玩法(直线型)最小花费为:84
路边玩法(直线型)最大花费为:129
操场玩法(圆型)最小花费为:81
操场玩法(圆型)最大花费为:130
*/

优化后代码


#include
using namespace std;
const int INF = 100000;
const int N = 205;
int Min[N][N], Max[N][N], s[N][N];//求最小、最大值
int sum[N];//计算前i堆石子的数量总和,sum[0]=0,w(i,j)=sum[j]-sum[i-1]
int a[N];//记录各堆石子的数量
int minn, maxx;

void get_Min(int n) {
	for (int v = 2; v <= n; v++) {//枚举合并的堆数规模
		for (int i = 1; i <= n - v + 1; i++) {
			int j = i + v - 1;
			int tmp = sum[j] - sum[i - 1];
			int i1 = s[i][j - 1] > i ? s[i][j - 1] : i;
			int j1 = s[i + 1][j] < j ? s[i + 1][j] : j;
			Min[i][j] = Min[i][i1] + Min[i1 + 1][j];
			s[i][j] = i1;

			for (int k = i1 + 1; k <= j1; k++) {
				if (Min[i][k] + Min[k + 1][j] < Min[i][j]) {
					Min[i][j] = Min[i][k] + Min[k + 1][j];
					s[i][j] = k;
				}
				
			}
			Min[i][j] += tmp;
		}

	}
}
void get_Max(int n) {
	for (int v = 2; v <= n; v++) {//枚举合并的堆数规模
		for (int i = 1; i <= n - v + 1; i++) {
			int j = i + v - 1;
			int tmp = sum[j] - sum[i - 1];
			Max[i][j] = -1;
			if (Max[i + 1][j] > Max[i][j - 1])
				Max[i][j] = Max[i + 1][j] + tmp;
			else
				Max[i][j] = Max[i][j - 1] + tmp;

		}

	}
}
void straight(int a[], int n) {
	for (int i = 1; i <= n; i++) {//初始化
		Min[i][i] = 0, Max[i][i] = 0, s[i][i] = 0;
	}
	sum[0] = 0;
	for (int i = 1; i <= n; i++) {
		sum[i] = sum[i - 1] + a[i];
	}
	get_Min(n);
	get_Max(n);
}

void Circular(int a[], int n) {
	for (int i = 1; i <= n-1; i++) {
		a[n + i] = a[i];
	}
	n = 2 * n - 1;
	straight(a, n);
	n = (n + 1) / 2;
	minn = Min[1][n];
	maxx = Max[1][n];
	for (int i = 2; i <= n; i++) {
		if (Min[i][n + i - 1] < minn) {
			minn = Min[i][n + i - 1];
		}
		if (Max[i][n + i - 1] > maxx) {
			maxx = Max[i][n + i - 1];
		}

	}

}

int main()
{
	int n;
	cout << "请输入石子的堆数 n:";
	cin >> n;
	cout << "请依次输入各堆的石子数:";
	for (int i = 1; i <= n; i++){
		cin >> a[i];
	}
	straight(a, n);
	cout << "路边玩法(直线型)最小花费为:" << Min[1][n] << endl;
	cout << "路边玩法(直线型)最大花费为:" << Max[1][n] << endl;
	Circular(a, n);
	cout << "操场玩法(圆型)最小花费为:" << minn << endl;
	cout << "操场玩法(圆型)最大花费为:" << maxx << endl;
	return 0;
}

1274:【例9.18】合并石子

这里用的是优化后的代码修改的,优化前的代码可能是时间复杂度的问题导致有两个测试点错误


#include
using namespace std;
const int INF = 100000;
const int N = 205;
int Min[N][N], Max[N][N], s[N][N];//求最小、最大值
int sum[N];//计算前i堆石子的数量总和,sum[0]=0,w(i,j)=sum[j]-sum[i-1]
int a[N];//记录各堆石子的数量
int minn, maxx;

void get_Min(int n) {
	for (int v = 2; v <= n; v++) {//枚举合并的堆数规模
		for (int i = 1; i <= n - v + 1; i++) {
			int j = i + v - 1;
			int tmp = sum[j] - sum[i - 1];
			int i1 = s[i][j - 1] > i ? s[i][j - 1] : i;
			int j1 = s[i + 1][j] < j ? s[i + 1][j] : j;
			Min[i][j] = Min[i][i1] + Min[i1 + 1][j];
			s[i][j] = i1;

			for (int k = i1 + 1; k <= j1; k++) {
				if (Min[i][k] + Min[k + 1][j] < Min[i][j]) {
					Min[i][j] = Min[i][k] + Min[k + 1][j];
					s[i][j] = k;
				}
				
			}
			Min[i][j] += tmp;
		}

	}
}
//void get_Max(int n) {
//	for (int v = 2; v <= n; v++) {//枚举合并的堆数规模
//		for (int i = 1; i <= n - v + 1; i++) {
//			int j = i + v - 1;
//			int tmp = sum[j] - sum[i - 1];
//			Max[i][j] = -1;
//			if (Max[i + 1][j] > Max[i][j - 1])
//				Max[i][j] = Max[i + 1][j] + tmp;
//			else
//				Max[i][j] = Max[i][j - 1] + tmp;
//
//		}
//
//	}
//}
void straight(int a[], int n) {
	for (int i = 1; i <= n; i++) {//初始化
		Min[i][i] = 0, Max[i][i] = 0, s[i][i] = 0;
	}
	sum[0] = 0;
	for (int i = 1; i <= n; i++) {
		sum[i] = sum[i - 1] + a[i];
	}
	get_Min(n);
	//get_Max(n);
}

//void Circular(int a[], int n) {
//	for (int i = 1; i <= n-1; i++) {
//		a[n + i] = a[i];
//	}
//	n = 2 * n - 1;
//	straight(a, n);
//	n = (n + 1) / 2;
//	minn = Min[1][n];
//	maxx = Max[1][n];
//	for (int i = 2; i <= n; i++) {
//		if (Min[i][n + i - 1] < minn) {
//			minn = Min[i][n + i - 1];
//		}
//		if (Max[i][n + i - 1] > maxx) {
//			maxx = Max[i][n + i - 1];
//		}
//
//	}
//
//}

int main()
{
	int n;
	//cout << "请输入石子的堆数 n:";
	cin >> n;
	//cout << "请依次输入各堆的石子数:";
	for (int i = 1; i <= n; i++){
		cin >> a[i];
	}
	straight(a, n);
	cout << Min[1][n];
	//cout << "路边玩法(直线型)最小花费为:" << Min[1][n] << endl;
	//cout << "路边玩法(直线型)最大花费为:" << Max[1][n] << endl;
	//Circular(a, n);
	//cout << "操场玩法(圆型)最小花费为:" << minn << endl;
	//cout << "操场玩法(圆型)最大花费为:" << maxx << endl;
	return 0;
}

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