动态规划初学
算法介绍
动态规划是一种求得最佳方案的算法,算法的前提是即该问题一定有最优解,在此前提下,假设所求状态存在最优解,则该状态的上一个状态一定存在最优解。如此下去,一直到边界,边界情况一般是确定的,然后递推下去,可求得任意状态的最优解。我的表达可能不太好,看下面的几个例子就知道了。
硬币找零
问题描述
给定几个不同面值的硬币,给定需要找零的钱数,不同面值的硬币可无限使用, 求出所需硬币最少的找零方案。比如给定硬币 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);
}