数学建模——递归和动态规划

发布于:2025-08-05 ⋅ 阅读:(10) ⋅ 点赞:(0)

1.问题引入

这个还得从实际问题出发

假设你在商店购物,最后需要用硬币支付,并且每次用的硬币数量要最少,并且总额不能超。而你现在有三种面额的硬币,分别是2,5,7

例如,假设你买了12块钱的东西.那么可能的方法有:

2+5+5,7+5,2*6,由于硬币数量要最少,因此7+5是最好的选择

那么怎么求出7+5这个答案呢,如果只有12块钱可能还好,但是假如现在有27块钱,那么可以选的方法就多了,直接枚举肯定行不通

2.递归

因此我们先从最后的答案出发

由于总额不能超27,又因为硬币只有三种面额,那除去最后一个硬币,之前所有硬币的价值就可以有三种情况(假设用了n枚硬币)

25+2(n-1) 22+5(n-1) 20+7(n-1)

现在假设有一个函数f(x)可以算最小硬币数,x代表总面额,那么就有

f(n)=min\left \{ f(n-2)+1 ,f(n-5)+1,f(n-7)+1\right \}

f(27)=min\left \{ f(27-2)+1 ,f(27-5)+1,f(27-7)+1\right \}

现在我们关心的就不再是f(27)而是f(20),f(22),f(25)

同理假如现在要求f(20)同样带入f(n)=min\left \{ f(n-2)+1 ,f(n-5)+1,f(n-7)+1\right \}

获得f(20)=min\left \{ f(20-2)+1 ,f(20-5)+1,f(20-7)+1\right \}

以此类推,直到找到已知条件比如\left\{\begin{matrix} f(0)=0 \\f(1)=inf \\ f(2)=1 \\ f(5)=1 \\ f(7)=1 \end{matrix}\right.

这就有点像树的结构,每一条分支到0就结束如果是负数或者到1就无效

不难发现其实我们要的最少个数的方法,就是层数最少那一条分支

function res=f(x)
if x==0
else
    res=inf;
    if x>=2
        res=min(f(x-2)+1,res);
    end
    if x>=5
        res=min(f(x-5)+1,res);
    end
    if x>=7
        res=min(f(x-7)+1,res);
    end
end
end

这里有个比较巧妙的地方,一开始说的【每一条分支到0就结束如果是负数或者到1就无效】

在哪里处理的?

我们就能看出来递归其实就是先从结果往初始推,再推回结果的过程

递归:递就是往初始推,归就是推回结果

虽然人看起来觉得代码简单,但是计算机要算的就多了

这里算27要知道20,22,25,算20要知道13,15,18,每增加一层就是3倍指数级的计算量增长

为啥多算了这么多,看第二行我们计算了20,22,25,看第三行我们又计算了20,甚至计算了两次,这下明白了,原来是重复计算的内容太多了,如果每次计算完一个数值存起来,那是不是后面就不用再算了,这样就简化了很多计算

3.动态规划

因此我们可以一开始就从初始向结果推导,把推导出的中间结果存起来便于后面使用,这种递推的方法就是动态规划

最开始有一个初始值

x <0 0
inf 0

同样的我们还是用这个公式

f(n)=min\left \{ f(n-2)+1 ,f(n-5)+1,f(n-7)+1\right \}(状态转移方程)

然后一个一个计算并把结果存起来以便下次使用

x <0 0 1 2 3 ... 27
inf 0 inf inf inf inf

f(1)=min\left \{ f(-6)+1, f(-4)+1,f(-1)+1\right \}=inf

x <0 0 1 2 3 ... 27
inf 0 inf inf inf inf

f(2)=min\left \{ f(-5)+1, f(-3)+1,f(0)+1\right \}=min\left \{ inf,inf,1 \right \}=1

x <0 0 1 2 3 ... 27
inf 0 inf 1 inf inf

f(3)=min\left \{ f(-4)+1, f(-2)+1,f(1)+1\right \}=inf

x <0 0 1 2 3 ... 27
inf 0 inf 1 inf inf

f(4)=min\left \{ f(-3)+1, f(-1)+1,f(2)+1\right \}=min\left \{ inf,inf,2 \right \}=2

x <0 0 1 2 3 4 ... 27
inf 0 inf 1 inf 2 inf

\vdots

f(27)=min\left \{ f(20)+1, f(22)+1,f(25)+1\right \}=5

x <0 0 1 2 3 ... 27
inf 0 1 inf 2 5
function res=DP(n)
dp=Inf(1,n+1);
dp(1)=0;
for i=1:n
    if i>=2
        dp(i+1)=dp(i-2+1)+1;
    end
    if i>=5
        dp(i+1)=min(dp(i+1),dp(i-5+1)+1);
    end
    if i>=7
        dp(i+1)=min(dp(i+1),dp(i-7+1)+1);
    end
end
if dp(n+1)~=Inf
    res=dp(n+1);
else
    res=-1;
end
end
   

这里主要是避免出现负数所以分段写

4.补充(背包问题)

这个算是背包问题,背包问题的dp其实是二维的dp[i][j]->dp[看到第i个物品为止][背包容量为j时]物品的最大价值

同样的我们利用前面的思想

这里干员数组

物品 1 2 3 4
工资 1 2 1 2
贡献值 2 1 1 3

dp初始数组

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1)
3(1)
4(3)

现在准备看第二个干员,当工资是0,或1时不需要考虑第二个干员,直接继承上面的数据,因为现在的工资不够第二个干员

当总工资是2时,显然不能同时放入两个干员,贡献值是max\left \{ dp[1][2] ,dp[1][0]+1\right \}意思是

要不就不要这个干员,现在总贡献值和只看了第一个干员的结果相同也就是dp[1][2]=2

要不就要第二个干员,但是给其他干员预留的工资只有总资金-这个干员的工资=2-2=0

那么最高贡献值就是当工资0只看该干员前面所有干员时的最高贡献值加上这个干员的贡献值,那招入第二个干员总贡献值最高就是dp[1][0]+1=1

显然最大贡献值是2 

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2
3(1)
4(3)

 当总工资是3时

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1)
4(3)

 现在准备看第三个干员,当工资是0时不需要考虑第三个干员,直接继承上面的数据,因为现在的工资不够第三个干员

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0
4(3)

当工资是1时,最大贡献值是max\left \{ dp[2][1] ,dp[2][0]+1\right \}=2

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2
4(3)

当工资是2时,最大贡献值是max\left \{ dp[2][2] ,dp[2][1]+1\right \}=3

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3
4(3)

当工资是3时,最大贡献值是max\left \{ dp[2][3] ,dp[2][2]+1\right \}=3 

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3
4(3)

 当工资是4时,最大贡献值是max\left \{ dp[2][4] ,dp[2][3]+1\right \}=4

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3 4 4 4
4(3)

 最后看第四个干员,同样的容量0,1继承

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3 4 4 4
4(3) 0 2

当工资是2时,最大贡献值是max\left \{ dp[3][2] ,dp[3][0]+3\right \}=3

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3 4 4 4
4(3) 0 2 3

当工资是3时,最大贡献值是max\left \{ dp[3][3] ,dp[3][1]+3\right \}=5 

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3 4 4 4
4(3) 0 2 3 5

当工资是4时,最大贡献值是max\left \{ dp[3][4] ,dp[3][2]+3\right \}=6

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3 4 4 4
4(3) 0 2 3 5 6

当工资是5时,最大贡献值是max\left \{ dp[3][5] ,dp[3][3]+3\right \}=6

干员(贡献值)\工资 0 1 2 3 4 5 6
1(2) 0 2 2 2 2 2 2
2(1) 0 2 2 3 3 3 3
3(1) 0 2 3 3 4 4 4
4(3) 0 2 3 5 6 6 7

 当工资是6时,最大贡献值是max\left \{ dp[3][6] ,dp[3][4]+3\right \}=7

看结果

 这下面是我拿c语言瞎写的代码,

#include<stdio.h>
int main() {
	int m, n;
	scanf("%d %d", &m, &n);
	int  ganyuan[100][100];
	int q[100];
	for (int i = 0; i < m; i++)
	{
		scanf("%d %d", &ganyuan[1+i][0],&ganyuan[1+i][1]);
	}
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &q[i]);
	}
	int dp[100][100];
	for (int i = 0; i < 10; i++)
	{
		dp[0][i] = 0;
	}
	for (int i = 1; i <= m; i++)
	{
		for (int j = 0; j < 10; j++)
		{
			if (ganyuan[i][0] > j) {
				dp[i][j] = dp[i - 1][j];
			}
			else 
			{
				if (dp[i-1][j- ganyuan[i][0]]+ ganyuan[i][1] > dp[i - 1][j])
				{
					dp[i][j] = dp[i - 1][j - ganyuan[i][0]] + ganyuan[i][1];
				}
				else
				{
					dp[i][j] = dp[i - 1][j];
				}
			}
		}
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", dp[m][q[i]]);
	}
	/*for (int i = 1; i <= m; i++)
	{
		for (int j = 0; j < 10; j++)
		{
			printf("%d ", dp[i][j]);
		}
		printf("\n");
	}*/
	return 0;
}

 勉强能跑,但是只能解决小数据的问题,如果像题目这种数据特别大,还得用其他方法优化


网站公告

今日签到

点亮在社区的每一天
去签到