算法导论读书笔记之钢条切割问题

算法导论读书笔记之钢条切割问题

巧若拙(欢迎转载,但请注明出处:http://blog.csdn.net/qiaoruozhuo

给定一段长度为n英寸的钢条和一个价格表 pi (i=1,2, …,n),求切割钢条的方案,使得销售收益rn最大。注意,如果长度为n英寸的钢条价格pn足够大,最优解可能就是完全不需要切割。

若钢条的长度为i,则钢条的价格为Pi,如何对给定长度的钢条进行切割能得到最大收益?

长度i   1   2    3   4     5      6     7     8      9     10

价格Pi   1    5   8   9    10     17    17    20    14    30

i = 1时,钢条不可切割,r[1]= 1;

i = 2时,钢条可分割为1+ 1,其价格为2。若不分割(0
+ 2),价格为5。即r[2] = 5;

i = 3时,钢条可分割为0+ 3,1 + 2。r[3]
= 8;

同理可得:

r[4] = 10(2+ 2);

r[5] = 13(2+ 3);

r[6] = 17(0+ 6);

r[7] = 18(1+ 6或4+ 3=> 2 + 2 + 3);

.......

我们可以发现,长度为7时,将其切割为长度4与长度3的钢条,并对两个钢条分别求最优解:长度4的最优解为r[4]
= 10(2 + 2),长度3的最优解为r[3] = 8,即可得r[7]
=r[4]+ r[3] =>原问题的最优解等于子问题的最优解之和的最大值

我们将钢条左边切割下长度为 i
的一段,只对右边剩下的长度为 n-i
的一段继续进行切割(递归求解),对左边的一段不再进行切割。即问题分解的方式为:将长度为n
的钢条分解为左边开始一段,以及剩余部分继续分解的结果。这样,不做任何切割的方案就可以描述为:第一段的长度为n
,收益为 pn,剩余部分长度为0,对应的收益为r0=0。于是公式的简化版本:

因此,在计算r[i]时,所求值即为r[0] +r[i],r[1]+ r[i- 1],r[2]+
r[i- 2],... 
,r[i- 1] +r[1]
之间的最大值,而在动态规划中,r[0]——r[i -1]的值在计算r[i]之前已经保存好了,进行少量的运算便能取得最优结果。

使用动态规划算法求解最优钢条切割问题有两种等价的实现方法。

第一种方法称为带备忘的自顶向下法。此方法按照递归方式编写过程,但过程会保存每个子问题的解——用数组r[i]记录总长度为i的钢管的最大收益值。当需要一个子问题的解时,过程首先检查是否已经保存过此解,如果是,直接返回保存的值,否则递归计算这个子问题。

第二种方法称为自底向上法,这是动态规划的常用方法,这种方法需要我们将子问题按照规模排序,按由小到大的顺序进行求解——在本题中即按i从1到n的顺序求解。每个子问题只需求解一次,并及时把结果记录下来,以便后面调用。

两种方法得到的算法具有相同的渐进运行时间,仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归的考察所有可能的子问题。由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂性函数通常具有更小的系数。

代码如下:

/*
	Name: 钢条切割问题
	Copyright:
	Author: 巧若拙
	Date: 12-12-14 19:49
	Description:
*/
#include<stdio.h>
#include<stdlib.h>

#define MAX 20001   //最大长度
#define INFINITY -999999   //负无穷大 

void PrintCutRodSolution(int p[], int n);//自底向上法
void BottomUpCutRod(int p[], int r[], int s[], int n);//记录总长度为i的钢管的最大收益值r[i]及其第一段钢条的切割长度s[i]
void MemoizedCutRod(int p[], int n);//带备忘的自顶向下法
int MemoizedCutRodAux(int p[], int r[], int s[], int n);//递归计算r[i]的值 

int main()
{
	int p[MAX] = {0};
	int i, n = 50;

	for (i=1; i<=n; i++)//随机获取长度价格,但确保长的比短的价值高
	{
		do{
			p[i] = (i>4)? (p[i-4] + rand()%20+1) : rand()%20+1;
		}while (p[i] <= p[i-1]);
		printf("%7d", p[i]);
	}

	printf("\n");
	PrintCutRodSolution(p, n);//自底向上法
	printf("\n");
	MemoizedCutRod(p, n);//带备忘的自顶向下法 

    return 0;
}

void PrintCutRodSolution(int p[], int n)//自底向上法
{
	int r[MAX] = {0};//r[i]记录总长度为i的钢管的最大收益值
	int s[MAX] = {0};//s[i]记录r[i]对应的第一段钢条的切割长度
	int i;

	BottomUpCutRod(p, r, s, n);
//	printf("最大价值:\n");
//	for (i=1; i<=n; i++)
//	{
//		printf("%2d:%3d  ", r[i], s[i]);
//	}
//	printf("\n");

	printf("最大价值: %d\n", r[n]);
	printf("切割方案:\n");
	while (n > 0)
	{
		printf("%d\t", s[n]);
		n -= s[n];
	}
	printf("\n");
}

void BottomUpCutRod(int p[], int r[], int s[], int n)//记录总长度为i的钢管的最大收益值r[i]及其第一段钢条的切割长度s[i]
{
	int i, j, max;

	r[0] = 0;
	for (i=1; i<=n; i++)//钢管总长度为i
	{
		max = INFINITY;
		for (j=1; j<=i; j++)
		{
			if (max < p[j] + r[i-j])//如果分解成长度j和长度i-j两部分能获得更大收益,则更新最大收益值max,并记录对应的第一段钢条的切割长度s[i]
			{
				max = p[j] + r[i-j];
				s[i] = j;
			}
		}
		r[i] = max; //记录总长度为i的钢管的最大收益值
	}
}

void MemoizedCutRod(int p[], int n)//带备忘的自顶向下法
{
	int r[MAX] = {0};//r[i]记录总长度为i的钢管的最大收益值
	int s[MAX] = {0};//s[i]记录r[i]对应的第一段钢条的切割长度
	int i;

	for (i=0; i<=n; i++)//初始化
		r[i] = INFINITY;

	printf("最大价值: %d\n", MemoizedCutRodAux(p, r, s, n));
	printf("切割方案:\n");
	while (n > 0)
	{
		printf("%d\t", s[n]);
		n -= s[n];
	}
	printf("\n");
}

int MemoizedCutRodAux(int p[], int r[], int s[], int n)//递归计算r[i]的值,并同时记录s[i]的值
{
	int i, max, temp;

	if (r[n] >= 0)//已经记录过了就不再重复计算
		return r[n];
	if (n == 0)
		max = 0;
	else
	{
		max = INFINITY;
		for (i=1; i<=n; i++)
		{
			temp = p[i] + MemoizedCutRodAux(p, r, s, n-i);//计算把钢条分为长度为i和n-i两部分所得到的最大收益值
			if (temp > max)//更新最大收益值并记录对应s[i]的值
			{
				max = temp;
				s[n] = i;
			}
		}
	}

	return r[n] = max;
}
时间: 2024-12-21 10:39:56

算法导论读书笔记之钢条切割问题的相关文章

算法导论读书笔记(17)

算法导论读书笔记(17) 目录 动态规划概述 钢条切割 自顶向下的递归实现 使用动态规划解决钢条切割问题 子问题图 重构解 钢条切割问题的简单Java实现 动态规划概述 和分治法一样, 动态规划 (dynamic programming)是通过组合子问题的解而解决整个问题的.分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解.与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题.在这种情况下,分治法会重复地求解公共的子子问题.而动态

算法导论读书笔记(15) - 红黑树的具体实现

算法导论读书笔记(15) - 红黑树的具体实现 目录 红黑树的简单Java实现 红黑树的简单Java实现 /** * 红黑树 * * 部分代码参考自TreeMap源码 */ public class RedBlackTree<T> { protected TreeNode<T> root = null; private final Comparator<? super T> comparator; private int size = 0; private static

算法导论读书笔记(16)

算法导论读书笔记(16) 目录 动态顺序统计 检索具有给定排序的元素 确定一个元素的秩 区间树 步骤1:基础数据结构 步骤2:附加信息 步骤3:维护信息 步骤4:设计新操作 动态顺序统计 之前介绍过 顺序统计 的概念.在一个无序的集合中,任意的顺序统计量都可以在 O ( n )时间内找到.而这里我们将介绍如何在 O ( lg n )时间内确定任意的顺序统计量. 下图显示的是一种支持快速顺序统计量操作的数据结构.一棵 顺序统计树 T 通过在红黑树的每个结点中存入附加信息而成.在一个结点 x 内,增

算法导论读书笔记(14) - 二叉查找树的具体实现

算法导论读书笔记(14) - 二叉查找树的具体实现 目录 二叉查找树的简单Java实现 二叉查找树的简单Java实现 /** * 二叉查找树 * 部分代码参考自TreeMap的源码 */ public class BinarySearchTree<T> { protected TreeNode<T> root = null; private final Comparator<? super T> comparator; private int size = 0; pub

算法导论读书笔记(13)

算法导论读书笔记(13) 目录 红黑树 旋转 插入 情况1 : z 的叔父结点 y 是红色的 情况2 : z 的叔父结点 y 是黑色的,而且 z 是右孩子 情况3 : z 的叔父结点 y 是黑色的,而且 z 是左孩子 删除 情况1 : x 的兄弟 w 是红色的 情况2 : x 的兄弟 w 是黑色的,且 w 的两个孩子都是黑色的 情况3 : x 的兄弟 w 是黑色的, w 的左孩子是红色的,右孩子是黑色的 情况4 : x 的兄弟 w 是黑色的,且 w 的右孩子是红色的 红黑树 红黑树 是一种二叉查

算法导论读书笔记(18)

算法导论读书笔记(18) 目录 最长公共子序列 步骤1:描述最长公共子序列的特征 步骤2:一个递归解 步骤3:计算LCS的长度 步骤4:构造LCS LCS问题的简单Java实现 最长公共子序列 某给定序列的子序列,就是将给定序列中零个或多个元素去掉后得到的结果.其形式化定义如下:给定一个序列 X = < x1 , x2 , - , xm >,另一个序列 Z = < z1 , z2 , - , zk >,如果 Z 满足如下条件则称 Z 为 X 的 子序列 (subsequence),

算法导论动态规划 15.1-3 钢条切割问题

算法导论的第一个动态规划问题--钢条切割 我们知道钢条的长度和价格为: 长度i 1 2 3 4 5 6 7 8 9 10 价格pi 1 5 8 9 10 17 17 20 24 30 书上的两种方法已经很清楚,这里主要说一下课后练习里面15-3钢条成本加上切割成本,即要切割的次数最少.15-4返回切割方案 #include<fstream> #include<iostream> using namespace std; int main() { int const N = 11;

算法导论读书笔记-第十四章-数据结构的扩张

算法导论第14章 数据结构的扩张 一些工程应用需要的只是标准数据结构, 但也有许多其他的应用需要对现有数据结构进行少许的创新和改造, 但是只在很少情况下需要创造出全新类型的数据结构, 更经常的是通过存储额外信息的方法来扩张一种标准的数据结构, 然后对这种数据结构编写新的操作来支持所需要的应用. 但是对数据结构的扩张并不总是简单直接的, 因为新的信息必须要能被该数据结构上的常规操作更新和维护. 14.1 动态顺序统计 顺序统计树(order-static tree) : 在红黑树的基础上, 在每个

平摊分析 --- 算法导论读书笔记

我们经常会说一个算法快不快,这个可以由实验得出,也可以通过分析复杂度得出.实验需要大量不同的输入才更全面准确,否则片面地看某个输入下的表现,是比较偏颇的.分析复杂度(通常分析最坏,因为平均涉及输入的概率分布,依靠假设或者实验和经验)有时候并不是一个简单的事,简单的情况是遍历 for(int i = 0; i != n; i++) 的这种情况,显然是O(n)的复杂度.但是一些复杂的情况就比较难办了,举例来说: a.   栈操作:  除了PUSH,POP,添加一个操作叫MULTIPOP. MULTI