以《换钱的方法数》来学动态规划及其优化

动态规划初步入门

近期在做程序员面试指南,里面有一道《换钱的方法数》问题,我觉得对理解动态规划和递归优化搜索很有帮助。

题目

给定数组arr,arr中所有的值都为正数且不重复。每一个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数CoinNum代表要找的钱数,求换钱的有多少种方法。

举例

arr=[5,10,25,1],CoinNum=0;

组成0元的方法有1种,就是所有货币都不用。所以返回1。

arr=[5,10,25,1],CoinNum=15;

组成15的方法有6种,分别为:

(1)3张5元

(2)1张10元+1张5元

(3)1张10元+5张1元

(4)10张1元+1张5元

(5)2张5元+5张1元

(6)15张1元

arr=[3,5],CoinNum=2;

任何方法都无法组成2元,所以返回0。

普通递归代码(O(coinnum^n))

#include "stdafx.h"
#include
#include
#include
using namespace std;
//leetcode518
int process1(vectorarr, int index, int aim)
{
	int res = 0;
	if (index == arr.size())
	{
		res = aim == 0 ? 1 : 0;
	}
	else
	{
		for (int i = 0; arr[index] * i <=aim; i++)
		{
			res += process1(arr, index + 1, aim - arr[index] * i);
		}
	}
	return res;
}
int coinsl(vectorarr, int aim)
{
	int res;
	if (arr.size() == 0 || aim < 0)
	{
		res = 0;
		return res;
	}
	else
	{
		return process1(arr, 0, aim);
	}
}

int _tmain(int argc, _TCHAR* argv[])
{
	vectorarr = { 5, 10, 25, 1 };
	int aim = 15;
	int res = coinsl(arr, aim);
	cout << res;
	return 0;
}

说明

在我看来,解决这个问题如果用普通递归的话,代码写好三点就好了:

  1. 代码递归的初识条件,在普通递归代码中,我们写的递归的初识条件就是

    int coinsl(vectorarr, int aim)
    {
    	int res;
    	if (arr.size() == 0 || aim < 0)
    	{
    		res = 0;
    		return res;
    	}
    	else
    	{
    		return process1(arr, 0, aim);
    	}
    }
    

    这样理解相当于在普通递归外套了一个“壳”

2.代码递归的结束条件,在普通递归代码中,我们写的递归的初识条件就是

if (index == arr.size())
	{
		res = aim == 0 ? 1 : 0;
	}

要给定递归结束的条件,比如说当前几个数已经组成完成钱数,这样就算1种方法

3、(最关键)循环条件

for (int i = 0; arr[index] * i <=aim; i++)
		{
			res += process1(arr, index + 1, aim - arr[index] * i);
		}

普通递归时间复杂度为O(coinnum^n)

记忆优化代码(O(N*coinnum^2))

#include"stdafx.h"
#include
#include
#include
#include
using namespace std;
int process2(vectorarr, int index, int aim, vector>map)
{
	int res=0;
	int i, j;
	int mapValue = 0;
	if (index == arr.size())
	{
		if (aim == 0)
		{
			res = 1;
		}
		else
		{
			res = 0;
		}

	}
	else
	{

		for (i = 0; i*arr[index] <= aim; i++)
		{
			mapValue == map[index + 1][aim - i*arr[index]];
			if (mapValue == 0)
			{
				res += process2(arr, index + 1, aim - arr[index] * i, map);

			}
			else
			{
				if (mapValue == -1)
				{
					res += 0;
				}
				else
				{
					res += mapValue;
				}
			}

		}
	}
	if (res == 0)
	{
		map[index][aim] = -1;
	}
	else
	{
		map[index][aim] = res;
	}
	return res;
}
int coinsl(vectorarr, int aim)
{
	vector>map(arr.size()+1,vector(aim+1));
	if (arr.size() == 0 || aim < 0)
	{
		return -2;
	}
	else
	{
		return process2(arr, 0, aim, map);
	}
}
int main()
{
	int a = 91;

	int aa = 92;
	vectorarr = { 5, 10, 25, 1};
	int aim = 15;
	int res = coinsl(arr, aim);
	cout << "换钱的方法数为:" << res << endl;
	return 0;
}

说明

记忆优化时间复杂为O(N*coinnum^2)

简述:暴力递归存在大量的重复计算,比如上面的例子,当已经使用0张5张+1张10元和使用2张5元和0张10元两种情况,剩下的钱组成15的可能,产生重复的计算,在暴力递归的过程中,这种计算会大量发生。记忆优化就是建立二维表map,每计算完一个递归过程,都将结果记录到map中,当下次进行也同样的递归过程之前,现在map中查询这个递归是否已经计算过。

动态规划代码(O(N*coinnum^2))

#include "stdafx.h"
#include
#include
#include
using namespace std;
int coinsl(vectorarr, int coinnum)
{
	if (arr.size() == 0 || coinnum < 0)
	{
		return 0;
	}
	vector>dp(arr.size()+1 , vector(coinnum + 1));
	int i, j,k;
	for (i = 0; i < arr.size() + 1; i++)
	{
		dp[i][0] = 1;
	}
	for (j = 0; j*arr[0]-coinnum<=0; j++)//这里要注意条件
	{
		dp[0][j*arr[0]] = 1;
	}
	int num = 0;
	for (i = 1; i < arr.size(); i++)
	{
		for (j = 1; j <=coinnum; j++)
		{
			num = 0;
			for (k = 0; j - k*arr[i] >= 0; k++)
			{
				num += dp[i - 1][j - k*arr[i]];
			}
			dp[i][j] = num;
		}
	}
	return dp[arr.size()-1][coinnum];
}
int main()
{
	int a = 91;
	int aa = 92;
	vectorarr = { 5, 10, 25, 1 ,2};
	int coinnum = 15;
	int res = coinsl(arr, coinnum);
	cout << "换钱的方法数为:"<< res<

说明

动态规划算法,时间复杂度为O(N*coinnum^2)生成行数为arr.size(),列数为coinnum+1的二维向量,

dp[i][j]

表示在使用arr[0…i]货币的情况下,组成钱数j有多少种方法。

二维向量第一行和第一列的值好确定

剩下的值为以下的值的累加

完全不用arr[i],只使用arr[0…i-1]货币时,方法数为dp(i-1,j)

用1张arr[i]货币,剩下的钱用arr[0…i-1]货币组成时,方法数为dp(i-1,j-arr[i])

用2张arr[i]货币,剩下的钱用arr[0…i-1]货币组成时,方法数为dp(i-1,j-2*arr[i])

用k张arr[i]货币时,剩下的钱用arr[0…i-1]货币组成时,方法数为dp(i-1,j-k*arr[i])

动态规划算法优化代码(O(N*coinnum)

for (i = 1; i < arr.size(); i++)
{
	for (j = 1; j <= coinnum; j++)
	{
		dp[i][j] = dp[i - 1][j];
		if (j - arr[i] >= 0)
		{
			dp[i][j]+= dp[i][j - arr[i]];//在这一步进行优化,第1种情况的方法数为dp[i][j-arr[i]]的值,从第2种情况到第k种情况的方法数其实就是累加dp[i][j-arr[i]]的值。
		}
		else
		{
			dp[i][j] += 0;
		}
	}
}

在使用动态规划优化算法时,其实就是省去了枚举的流程,与普通动态规划相比,不用k的变量了,时间复杂度为O(N*coinnum)。

动态规划算法进一步优化(时间O(N*coinnum,空间O(coinnum))

for (int j = 0; arr[0] * j <= coinnum; j++)
{
	dp[arr[0] * j] = 1;
}
for (int i = 1; i < arr.size(); i++)
{
	for (int j = 1; j < coinnum; j++)
	{
		if (j - arr[i] >= 0)
		{
			dp[j] += dp[j - arr[i]];
		}
		else
		{
			dp[j] += 0;
		}
	}
	return dp[coinnum];
}

在上一步优化的基础上,利用空间压缩原理,时间复杂度为O(N*coinnum),空间复杂度为O(coinnum)

补充:空间压缩原理

通过一个数组滚动更新的方式节省了空间,这样很好。

你可能感兴趣的:(算法,剑指offer,语言编程,动态规划)