动态规划初识

本文深入讲解动态规划算法,包括硬币找零、背包问题及游艇租赁问题等经典案例的求解思路与代码实现,帮助读者掌握动态规划的核心原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划初学

算法介绍

动态规划是一种求得最佳方案的算法,算法的前提是即该问题一定有最优解,在此前提下,假设所求状态存在最优解,则该状态的上一个状态一定存在最优解。如此下去,一直到边界,边界情况一般是确定的,然后递推下去,可求得任意状态的最优解。我的表达可能不太好,看下面的几个例子就知道了。

硬币找零
问题描述

给定几个不同面值的硬币,给定需要找零的钱数,不同面值的硬币可无限使用, 求出所需硬币最少的找零方案。比如给定硬币 1,4,5,找零数为 8,则显而易见两个面值为 4 的硬币即为所求方案。

问题分析

第一次看到该问题的时候,我想到的是用贪心法求解,即在找零的过程中,把当前可允许加入方案的硬币的最大值加入方案,比如对于上面的 1,4,5 硬币,找零数为 8,若用贪心法求,则方案为 [ 5, 1, 1, 1],显然两个 4 即可满足条件。所以贪心法不能一定求出最优解,有些情况可以,需要一定的条件,在此不叙述。下面说一下用动态规划求解该问题的思路。

动态规划求解硬币找零(递推)

对于动态规划求解问题,首先要知道该问题的动态方程,所谓动态方程,即使前一个状态与后一个状态的关系。对于该问题,分析可得,若存在给定找零钱数 sum,存在找零硬币数目最小的解 f ( sum ),那么对于找零数 sum - coin[ i ],则一定存在找零数目最小的解 f ( sum - coin [ i ] )。所以该问题的动态方程即为:f ( n ) = f ( n - coin[ i ] ) + 1,其中 coin [ i ]表示可选择的找零硬币。可以看到在这里强调了 递推,所谓的递推,即是从边界状态开始求解(因为边界情况可直接求得),然后根据边界状态的解逐渐递增求得其他状态的解,直到求得所需状态的解。

分析下该问题,不断递增的是需要找零的金额,所以可以将找零的金额从 1 开始,不断递增 ,每次加 1,直到求得给定找零金额的值。对于当前金额 n,因为本题要求为求得最少硬币数,所以首先考虑硬币数为 1 的情况(即从给定硬币中找寻面值等于当前找零数),若存在,则查找完毕,若不存在,则从 硬币找零数为 1 ~ ( n - 1 )中的最优方案中查询是否存在硬币找零数为 1 的方案 f ( n - x ),若存在,则从给定找零硬币中查询是否存在某个面值的硬币 coin[ i ]+ ( n - x ) <= n,存在的话找到那个 max( coin [ i ] ),即为 f ( n )的最佳方案,不存在的话那就找硬币数为 2 的方案 f ( n - x )如此下去,在此说的不太明白,直接看代码。

代码示例

在此存放方案的数据结构为二位数组,行代表的是需要找零的金额,列代表的是给定硬币的下标。

#include<stdio.h>
#include<stdlib.h>
int coin[4] = {1, 2, 4, 5};  /* 给定的硬币面值  */
int coin_num = 4;			 /* 给定的硬币种类数目 */ 
int sum = 8;				 /* 给定的需要找零的钱数 */ 
int plan[20][5];			 /* 存放找零方案的数组 */ 

/* 	初始找零方案 */
void initPlan(){
	for (int i = 0; i < 20; i++)
	{
		for (int j = 0; j < coin_num; j++)
		{
			plan[i][j] = 0;
		}
	}
} 

/* 输出找零方案 */ 
void output() {
	printf("    ");
	for (int i = 0 ; i < 4; i++)
	{
		printf("%d  ",coin[i]);
	}
	printf("num\n");
	for (int i = 0; i < 20; i++)
	{
		if (i < 10) printf("%d   ", i);
		else printf("%d  ",i);
		for (int j = 0; j < 5; j++)
		{
			printf("%d  ", plan[i][j]);
		}
		printf("\n");
	}	
}
/*  复制数组的行(看下后面即明白干啥用的) 
	@param    money1     前一个状态 
	@param    money2     当前状态
	@param    coin_value 需要的硬币面值 
*/ 
void copyRow(int money1, int money2, int coin_value) {
	int coin_index;
	for (int i = 0; i < coin_num; i++)
	{
		if (coin[i] == coin_value) coin_index = i;
		plan[money2][i] = plan[money1][i];
	}
	plan[money2][coin_index] ++;
	plan[money2][coin_num] = plan[money1][coin_num] + 1;
	
}
/* 查找主函数 */
void find(){
	int money = 1;		 		/* 声明找零数 从 1 开始累加  */
	int num   = 1;				/* 声明找零硬币数目,从 1 开始  */
	bool is_exist = false;		/* 标志位 -> 是否找到 */  
	while (money <= 20) 
	{
		num = 1;
		while (true)
		{
			is_exist = false;
			if (num == 1)		/* 若找零数为 1,则直接遍历找零硬币数组 */
			{
				for (int i = 0; i < coin_num; i++)
				{
					if (coin[i] == money)
					{
						plan[money][i] = 1;
						plan[money][coin_num] = 1;
						is_exist = true;
						break;
					}
				}
			}else 
			{
				/*  一枚硬币不能满足当前状态,遍历上面的状态,找零数目从 2 开始
					若找到当前的找零硬币数 n ,则遍历找零硬币数组,看是否存在满足当前的找零数
					在此 因为 存在面值为 1 的硬币,则一定存在。存在的话复制上一个状态的方案,
					然后新增的那个硬币数目 + 1 
				*/
				for (int i = 1; i < money; i++)
				{
					if (plan[i][coin_num] == num - 1)
					{
						for (int j = 0; j < coin_num; j++)
						{
							if (i + coin[j] == money)
							{
								is_exist = true;
								copyRow(i, money, coin[j]);
							}
						}
					}
					if (is_exist) break;
				}
			}
			if (!is_exist) num ++;
			else break;
		
		}
		money ++;
	}
}
int main(){
	initPlan();
	find();
	output();
}
背包问题
问题描述

给定几个物品,每个物品都有其重量 w 和 价值 v, 给定一个背包,其容量为 c,
用该背包装物品,求得装的物品价值之和为最大值的方案。

问题分析

既然本文章讲的是动态规划,下面就用动态规划的思想分析下,对于给定背包的容量 c,若存在装物品的最大价值 f ( c ),则对于 c - w ,则一定存在其装物品的最大价值 f ( c - w )。所以动态方程为:f ( c ) = ( c - w ) + v,其中 w 和 v 为某个物品的重量和价值。下面说下求解思路。

动态规划求背包问题(递推)

在此还是用的递推,对于递推要从边界开始,所以假设从背包容量为 1 开始,逐渐增加,直到给定的背包容量。对于当前背包容量 c ,首先设置其初始最大价值为 背包容量为 0 的最大价值 , 即为0,设置临时存放方案的数组。遍历容量为 1 ~ ( c - 1 )的最优方案,并且遍历过程中,找到该容量背包未装入且重量之和不超过 c 背包的容量的最大价值的物品,若此时最大价值大于 f ( c ),则更新临时存放方案的数组,直到遍历完毕,临时数组中即存放的当前所求背包容量 c 的最优方案。下面看代码

代码示例( c语言 )

示例代码用的数据结构是二位数组,行表示背包容量,列表示存放物品数组的下标。然后用两个一维数组存放物品的重量和价值,下标对应。

#include<stdio.h>
#include<stdlib.h>
int goods_kind_num  = 10;							/* 物品的数目 */
int goods_weight[10] = {1,3,5,6,10,11,15,18,19,25};	/* 物品的重量 */ 
int goods_value[10]  = {1,5,8,11,19,20,30,36,35,40};/* 物品的价值 */ 
int pack_content    =  18;							/* 背包容量(在此我把全部重量遍历出来了,所以没用)*/
int plan[100][11];									/* 存放方案的数组 */
/* 获取所有列出物品的重量之和 */ 
int AllGoodsWeight() {
	int goods_weight_sum = 0;
	for (int i = 0; i <goods_kind_num; i++)
	{
		goods_weight_sum += goods_weight[i];
	}
	return goods_weight_sum;
}
/* 初始化方案数组 */
void initPlan() {
	int all_good_weight = AllGoodsWeight();
	for (int i = 0; i < all_good_weight; i++)
	{
		for (int j = 0; j <= goods_kind_num; j++)
		{
			plan[i][j] = 0;
		}
	}
}
/*  打印方案
	@param   index    指定容量 ,若不指定则打印所有重量的方案 
	 
 */
void outputPlan(int index = -1) {
	if (index != -1)
	{
		int maxValue = 0;
		int maxWeight = 0;
		printf("包的容量为:%d Kg\n", index);
		for (int i = 0; i < goods_kind_num; i++)
		{
			if (plan[index][i] == 1)
			{
				printf("重量:%d,价值:%d\n", goods_weight[i], goods_value[i]);
				maxValue += goods_value[i];
				maxWeight += goods_weight[i];
			}
		}
		printf("总重量:%d,总价值:%d\n", maxWeight, maxValue);
		printf("\n");
	}else
	{
		int all_good_weight = AllGoodsWeight();
		for (int i = 0; i < all_good_weight; i++)
		{
			outputPlan(i);
		}
				
	}
}
/* 清除给定的容量方案 */
void clearRow(int weight) {
	for (int i = 0; i <= goods_kind_num; i++)
	{
		plan[weight][i] = 0;
	}
}
/* 新背包 = 老背包 + maxValue(物品) */
int getMaxValueGoodIndex(int old_pack, int new_pack) {
	int maxValue = plan[old_pack][goods_kind_num];
	int index = -1;
	for (int i = 0; i < goods_kind_num; i++)
	{
		if (
			plan[old_pack][i] == 0 && 
			old_pack + goods_weight[i] <= new_pack &&
			plan[old_pack][goods_kind_num] + goods_value[i] > maxValue 
		){
			maxValue = plan[old_pack][goods_kind_num] + goods_value[i];
			index = i;
		}
	}
	return index;
}
/* 复制背包容量 (因为新背包容量 与 老背包容量 只差一个物品 )*/
void copyPackage(int old_pack, int new_pack) {
	for (int i = 0; i <= goods_kind_num; i++)
	{
		plan[new_pack][i] = plan[old_pack][i];
	}
}
/* 查找主函数 */ 
void find() {
	int package = 1;
	int all_good_weight = AllGoodsWeight();
	initPlan();
	while (package < all_good_weight) {
		
		int maxValue = plan[0][goods_kind_num];
		for (int i = 0; i < package; i++)
		{
			int index = getMaxValueGoodIndex(i, package);
			
			if (index == -1)
			{
				if (plan[i][goods_kind_num] >= maxValue)
				{
					maxValue = plan[i][goods_kind_num];
					copyPackage(i, package);
				}
			}else
			{
				if (plan[i][goods_kind_num] + goods_value[index] > maxValue)
				{
					maxValue = plan[i][goods_kind_num] + goods_value[index];
					copyPackage(i, package);
					plan[package][index] = 1;
					plan[package][goods_kind_num] += goods_value[index];
				}
			}
		}
	
		package ++;
		
	}
	
}
int main() {
	find();
	outputPlan();
}
游艇租赁问题
问题描述

在长江的一段水域中,设置了几个游艇租赁站,上游的租赁站只能到下游的租赁站,反之不行,每个租赁站到其他租赁站都有相应的价格,给定站 1,站 2,求得花钱最少的站点选择方案。

问题分析

这个问题就是低配版的选择火车中转,问题分析和上面两个一样,不再详细叙述。首先你先得出动态方程,然后从边界开始。下面直接贴个示例代码,在此不建议以代码为准,感觉还是把思想想明白,然后自己码,这样才能掌握其中的知识。代码知识示例 ~代码只是示例 ~代码只是示例…

示例代码
#include<stdio.h>
/*
	 			 A   B   C   D    E
			 A 	 0   5   7   15   16
			 B     -1   0   4   10   12
			 C  	-1  -1   0   8    14
			 D  	-1  -1  -1   0    5
			 E  	-1  -1  -1  -1    0  


*/


int stop_num = 5;
char stop[5] = {'A', 'B', 'C', 'D', 'E'};
int stop_list[5][5] = {
						{0, 5, 7, 15, 16},		 	
						{-1, 0, 4, 10, 12},
						{-1, -1, 0, 8, 14},
						{-1, -1, -1, 0, 5},
						{-1, -1, -1, -1, 0}
					  };
int travel_plan[5][6];
void initTravelPlan() {
	for (int i = 0; i < stop_num; i++)
	{
		for (int j = 0; j < stop_num; j++)
		{
			travel_plan[i][j] = 0;
		}
	}
}
void outputStopList() {
	for (int i = 0; i < stop_num; i++)
	{
		for (int j = 0; j < stop_num; j++)
		{
			if (stop_list[i][j] >= 0 && stop_list[i][j] < 10) printf("%d   ", stop_list[i][j]);
			else printf("%d  ", stop_list[i][j]);
		}
		printf("\n");
	}
}
void outputTravelPlan() {
	for (int i = 0; i < stop_num; i++)
	{
		for (int j = 0; j < stop_num + 1; j++)
		{
			printf("%d   ", travel_plan[i][j]);
		}
		printf("\n");
	}
}
void copyRow(int last, int after) {
	for (int i = 0; i < stop_num; i++)
	{
		travel_plan[after][i] = travel_plan[last][i];
	}
}
void search(char start, char end) {
	initTravelPlan();
	int startIndex, endIndex;
	int index;
	int minIndex;
	int minPrice;
	for (int i = 0; i < stop_num; i++)
	{
		if (start == stop[i])
		{
		  startIndex = i;
		  continue;	
		}
		if (end == stop[i])
		{
			endIndex = i;
			continue;
		}
	}
	index = startIndex + 1;
	while (index <= endIndex)
	{
		minIndex = startIndex;
		minPrice = stop_list[startIndex][index];
		for (int i = startIndex; i < index; i++)
		{
			if (travel_plan[i][5] + stop_list[i][index] < minPrice)
			{
				minIndex = i;
				minPrice = travel_plan[i][5] + stop_list[i][index];
			}
		}
		copyRow(minIndex, index);
		travel_plan[index][minIndex] = 1;
		travel_plan[index][5] = minPrice;
		
		index ++;
	}
	outputTravelPlan();
	
}
int main() {
	char start, end;
	printf("输入起始站:");
	scanf("%c", &start);
	char c = getchar();
	printf("输入终点站:");
	scanf("%c", &end);
	search(start, end);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值