USACO_2_3_Money Systems

又是动态规划,又是动规方程,但是如果你是准备参加noip的选手的话请不要错过这篇解说,因为这倒题虽然简单,但有很大的启发性,因此我准备详细的通过三个代码来讲解这道题。

首先我会引导读者如何去思考这道题的动规方法,通过一个时间效率为O(v*n*n)、空间效率为O(V*n)的简单方法,让读者理解程序的正确性。

然后我将改变一下思考的角度,介绍一个时间效率为O(v*n)、空间效率为O(v*n)的方法,让读者注意到对于同样的方法当考虑角度稍有变化时如何影响到算法的效率。

最后我将介绍一种时间效率为O(v*n)、空间效率为O(n)的方法,指出在动态规划中一种减少空间使用量的常用方法,这对参加编程比赛是很有用的。

注:ways[i][j]表示在前i种硬币中,拿出总值为j元的方案数

方法1:

还是先模拟过程,然后从过程的最后一步考虑起。注意,这里所说的“模拟”,就是在告诉读者如何去思考这个问题。

可以这样来模拟:有一排桌子,共有v个,每张桌子上面都有一堆拿不完的硬币,但是每张桌子上的硬币都是相同面额的,我从第一张桌子顺序走到最后一张桌子,每路过一张桌子的时候我可以拿取这张桌子上面的任意个硬币,直到路过所有桌子后我手中的硬币总额必须为n元。

考虑最后一步:如果我在第i张桌子上拿了m个硬币,然后就不再在其它的桌子上拿硬币了,且我手中的钱正好是n元,那么最后一步就是“在第i张桌子上面拿取了m个硬币”

在最后一步中,我可以拿0个或多个硬币,这就是最后一步的选择,那么选择拿取m个硬币后剩下的子问题就是从前i-1张桌子上拿取j-m×units[i]钱有多少种方法,其中units[i]是第i张桌子上硬币的面额。

因为每种选择都是独立可行的方法,因此有动规方程: ways[i][j] = Sum(ways[i - 1][j - m * units[i]]) {m = 0、1、2、……、k},k是最多可以拿取的个数。

注意到,这里问题数就是ways变量的个数,等于v * n,而选择数最坏情况下为n,因此最坏情况下时间效率为O(v*n*n)

详细程序见代码 code1

ContractedBlock.gif ExpandedBlockStart.gif Code1
ExpandedBlockStart.gifContractedBlock.gif/**//*
ID: sdjllyh1
PROG: money
LANG: JAVA
complete date: 2008/12/18
complexity: O(v * n * n)
author: LiuYongHui From GuiZhou University Of China
more article: www.cnblogs.com/sdjls
*/


import java.io.*;
import java.util.*;

public class money
ExpandedBlockStart.gifContractedBlock.gif
{
    
private static int v, n;
    
private static long[][] ways;//ways[i][j]表示在前i种硬币中,拿出总值为j元的方案数
    private static int[] units;//硬币的面值
    private static long answer;

    
public static void main(String[] args) throws IOException
ExpandedSubBlockStart.gifContractedSubBlock.gif    
{
        init();
        run();
        output();
        System.exit(
0);
    }


    
private static void run()
ExpandedSubBlockStart.gifContractedSubBlock.gif    
{
        
//处理边界,当总共要拿j元时,如果j是第一种硬币的整数倍,那么ways[0][j]=1
        for (int j = 1; j <= n; j++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            
if (j % units[0== 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                ways[
0][j] = 1;
            }

            
else
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                ways[
0][j] = 0;
            }

        }

        
//处理边界,当总共要拿0元时,方案数为1
        for (int i = 0; i < v; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            ways[i][
0= 1;
        }

        
//求ways[i][j]
        for (int i = 1; i < v; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            
for (int j = 1; j <= n; j++)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                
int multiple = 0;//第i种硬币拿取的个数
                
//当从第i种硬币中得到的钱不超过j时
                while (multiple * units[i] <= j)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
{
                    ways[i][j] 
+= ways[i - 1][j - multiple * units[i]];
                    multiple
++;
                }

            }

        }


        answer 
= ways[v - 1][n];//注意到i是从0循环到v-1
    }

    
    
private static void init() throws IOException
ExpandedSubBlockStart.gifContractedSubBlock.gif    
{
        BufferedReader f 
= new BufferedReader(new FileReader("money.in"));
        StringTokenizer st 
= new StringTokenizer(f.readLine());
        v 
= Integer.parseInt(st.nextToken());
        n 
= Integer.parseInt(st.nextToken());

        units 
= new int[v];
        
int index = 0;
        String readLine 
= f.readLine();
        
while (readLine != null)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            st 
= new StringTokenizer(readLine);    
            
while (st.hasMoreTokens())
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                units[index] 
= Integer.parseInt(st.nextToken());
                index
++;
            }

            readLine 
= f.readLine();
        }

        f.close();

        ways 
= new long[v][n + 1];
    }


    
private static void output() throws IOException
ExpandedSubBlockStart.gifContractedSubBlock.gif    
{
        PrintWriter out 
= new PrintWriter(new BufferedWriter(new FileWriter("money.out")));
        out.println(answer);
        out.close();
    }

}



方法2:

如果我们在模拟过程中稍微做一点变动就会发现一种效率更高的算法,如下:

在方法一的模拟过程中,对于每一步的描述可以表达如下“在第i张桌子上我应该拿取多少个硬币?”,现在改为“在第i张桌子上我是否应该再拿取一个硬币?(如果不拿,那就走向下一张桌子)”

此时思考的角度就从“拿多少个(选择数为O(n))”到“拿与不拿(选择数为O(1))”,可见选择数变少了,但是子问题发生了变化。

方法1的子问题可以表达如下“在前i-1张桌子上拿取总额为j-m*units[i]的方法数”,而方法2的子问题变为“当再拿取一个硬币时,在前i张桌子上拿取总额为j - units[i]的方法数”与“不再拿硬币时,在前i张桌子上拿取总额为j的方法数”,至于“最优子结构”问题读者自己证明。

因此可得如下动规方程:ways[i][j] = ways[i][j-units[i]] + ways[i-1][j],ways[i][j-units[i]]是再拿一个的情况,ways[i-1][j]是不再拿走向下一张桌子的情况。 (提示:设在第i张桌子上拿取了m个硬币,当m>0时, 所有的方法都被ways[i][j-units[i]]包含了,因此当走向下一张桌子时仅需要考虑m=0的情况。)

可见子问题数没变而选择数减少了一个数量级,因此时间效率提高到O(v*n)

主要被改动的代码见 code2

 

ContractedBlock.gif ExpandedBlockStart.gif Code2
    private static void run()
ExpandedBlockStart.gifContractedBlock.gif    
{
        
        
for (int j = 1; j <= n; j++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            
if (j % units[0== 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                ways[
0][j] = 1;
            }

            
else
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                ways[
0][j] = 0;
            }

        }

        
        
for (int i = 0; i < v; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            ways[i][
0= 1;
        }

        
        
for (int i = 1; i < v; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            
for (int j = 1; j <= n; j++)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                
//如果第i种硬币可以拿,即总共要拿取的钱数大于等于第i种硬币的面额
                if (j - units[i] >= 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
{
                    ways[i][j] 
= ways[i - 1][j] + ways[i][j - units[i]];
                }

                
else
ExpandedSubBlockStart.gifContractedSubBlock.gif                
{
                    ways[i][j] 
= ways[i - 1][j];
                }

            }

        }


        answer 
= ways[v - 1][n];
    }



方法3:

注意到方法2中的动规方程:ways[i][j] = ways[i][j-units[i]] + ways[i-1][j]

我们在求ways[i][*]时仅会用到ways[i-1][*],而不会用到ways[i-2][*],ways[i-3][*]等等。

这就表示,任何时刻我们都可以仅用两个数组来保存ways的值,而不用v个,公式就可以简化为: ways_2[j] = ways_2[j-units[i]] + ways_1[j]。

且在求ways_2[j]时,ways_2[j]的值可以是任意值而不会影响到ways_2[j]的正确性(因为它的值是由ways_2[j-units[i]]与ways_1[j]决定的),

那么我们就可以用ways_2[j]的来保存ways_1[j]的值,公式可以改为: ways_2[j] = ways_2[j-units[i]] + ways_2[j]。

注意,当计算ways_2[j] = ways_2[j-units[i]] + ways_2[j]时,等号左边的ways_2[j]表示“前i张桌子拿取j元的方案数”,而等号右边的ways_2[j]表示“前i-1张桌子拿取j元的方案数”。

这就只需要用一个大小为O(n)的ways数组了。空间效率从O(v*n)提高到了O(n)。

主要被改动的代码见 code3

ContractedBlock.gif ExpandedBlockStart.gif Code3
    private static void run()
ExpandedBlockStart.gifContractedBlock.gif    
{
        
        
for (int j = 0; j <= n; j++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            
if (j % units[0== 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                ways[j] 
= 1;
            }

            
else
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                ways[j] 
= 0;
            }

        }

        
        
for (int i = 1; i < v; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{
            
for (int j = 1; j <= n; j++)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                
//如果第i种硬币可以拿,即总共要拿取的钱数大于等于第i种硬币的面额
                if (j - units[i] >= 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
{
                    ways[j] 
= ways[j] + ways[j - units[i]];
                }

                
else
ExpandedSubBlockStart.gifContractedSubBlock.gif                
{
                    ways[j] 
= ways[j];
                }

            }

        }


        answer 
= ways[n];
    }

转载于:https://www.cnblogs.com/SDJL/archive/2008/12/18/1357635.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值