贪心算法引申的非常规硬币的凑钱问题

目录

问题:

 初期解决方案:

 二次解决方案:

真·解决方案:

源码下载:


 

问题:

现在有 1元,2元,5元 这三种硬币若干。那么如何才能用最少的硬币达到需要的金额?

 

 初期解决方案:

上述问题其实很好解决,用最传统的贪心算法即可。在这三种硬币中不断地从大到小循环每次用小等于的面值减去剩余的面值直至完全排列出来即可,代码也很简单

var coins = new int[] { 1, 2, 5 };
for (int i = 0; i < coins.Length; i++) //用冒泡反向排个序
{
    for (int j = i + 1; j < coins.Length; j++)
    {
        if (coins[j] > coins[i])
        {
            var temp = coins[j];
            coins[j] = coins[i];
            coins[i] = temp;
        }
    }
}

var money = 13;//所需金额
var tempMoney = 0;
var temporary = new List<int>();
while (true)
{
    foreach (var coin in coins)
    {
        if (money - tempMoney >= coin)
        {
            tempMoney += coin;
            temporary.Add(coin);
            break;
        }
    }
    if (tempMoney == money)
    {
        break;
    }
}
Console.Write("所需最少硬币组和为:");
foreach (var item in temporary)
{
    Console.Write(item + " ");
}


输出结果为: 所需最少硬币组和为:5 5 2 1

后来又试了不同的值,结果很完美。并没有出现什么问题。

但贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。理论上应该会有局限才对啊?后来查了下资料发现(看看就成):

我国现行流通使用的人民币共有12种面值,这就是100元、50元、10元、5元、2元(已经很难见到了)、1元、5角、2角、1角、5分、2分、1分。从所发行的人民币中,人们清楚地了解到没有3、4、6、7、8、9这些数的面值。这是为什么呢?原来,在1--10这10个自然数里,有“重要数”和“非重要数”两种,1、2、5、10就是重要数。用这几个数就能以最少的加减组成另一些数。如1+2=3、2+2=4、1+5=6、2+5=7、10-2=8、10-1=9。如将四个“重要数”中任何一个数用“非重要数”代替,那将出现有的数要两次以上的加减才能组成的繁琐现象。

 

 二次解决方案:

由于1、2、5的特殊性,所以传统的贪心算法可以说完美的适应真实面额的凑钱问题。既然找零只是用来实现算法的例子,所以还要继续挖掘,那干脆再加个面值为4块钱的硬币,于是问题变为:

现在有 1元,2元,4元,5元 这四种硬币若干。那么如何才能用最少的硬币达到需要的金额?

 

再用上述的方法问题就暴露出来了,当我需要8块钱时算法给出的结果为:[5, 2, 1] 。很显然稍微有点常识的人都能算出只要两枚4块钱的硬币即可。通过不懈的努力终于给倒腾出来了:

var coins = new int[] { 5, 4, 2, 1}; //排过序的

var money = 8;//所需金额
var tempMoney = 0;
var temporary = new List<int>();

var m = monery / coins[0];
//如果为2以内倍数
if (m < 2 || (m == 2 && d % item == 0))
{
    if (d % item == 0)
    {
        for (int ii = 0; ii < m; ii++)
        {
            temporary.Add(item);
            i += item;
        }
    }
    else
    {
        foreach (var c in coins)
        {
            foreach (var cc in coins)
            {
                if (d == c + cc)
                {
                    temporary.Add(cc);
                    temporary.Add(c);
                    i += c;
                    i += cc;
                    break;
                }
                if (i >= n)
                {
                    break;
                }
            }
            if (i >= n)
            {
                break;
            }
        }
    }
}
else
{
    for (int ii = 0; ii < m - 1; ii++)
    {
        temporary.Add(item);
        i += item;
    }
    break;
}

//最大的问题果真还是命名啊。随便看看就成,别在意细节

通过贪心算法发现每次都会用最大的面值去和剩余金额对比如果大于则添加。所以我们可以稍微的优化下:先用目标金额除以硬币中最大面值,当倍数大于2且有余数时可以用倍数-1然后除去这部分,再拿剩余的部分筛选。为什么要选两倍的余数呢?因为当两个数比较时较小的数的三倍除较大数永远小于3。简单举例就是8大于5但它可以由两个4合成,加上自身再加上余数就得到一个小于2的值。那这有啥用说着啥原理呢?不知道,反正能用就成不能用再改方案。嗯,这样就去掉的多余的数,只剩下需要整理的部分了,简单算了下它给计算机带来的性能提升是巨大的(虽然并没有感觉出来,哈哈)。//额我在说啥,不管了你们假装听懂就成了,听不懂就直接略过吧    ( ̄y▽, ̄)╭ 

 

感觉这次有戏于是加个循环,填值运行。当所需金额为8时完美输出了两个4而不是5、2、1。然后继续往下跑结果很成功,那么其他的值也应该可以了吧。硬币中不是还有个值为10的吗?加上试试……

 4 => [2, 2]
10 => [5, 5]
24 => [5, 5, 5, 5, 2, 2]
...

这是啥???

大致问题为10正好是上个值5的倍数,然后应该可以用10解决的问题它嫩是拆分成两个5处理……后面开始无休止的调试,最后发现越调坑越大,终于在某次大脑短路后基本放弃了。

 

真·解决方案:

这种想法及解法应该很常见吧,没头绪那只能直接去网上冲浪了。看了不少背包算法的动态规划法,但怎么说呢,是解决了我的问题。但无乎找到一个最优解然后再回头穷尽法,不是我的菜。如果找不到更好方法再去研究吧。找吧找吧终于找到了一个,此处 传送门 传送门 传送门 重要的事说三遍!

他的方案乍一看很难,其实研究下很简单:就是每次从0开始穷举每个自增值的最优方案,然后下一个值在之前的方案中选择能凑成所需值的值,顾它只计算之前最优递增的数据所以说性价比还是很高的。PS:这个解法简直太帅了

d(i) = min(d(i-u_{x})+1),(i-u_{x}\leq 0)     (u_{x}表示硬币的币值)

 核心解法及过程就不多阐述了,可以去原博看,否则算纯搬运了(虽然本来就是搬运,但读书人的事能算抄吗?这叫借鉴、思维拓展、分享精神、开源运动的拥护者……)

 但原博的方案在我这不能直接用还要细细品味后再简单修饰下:

private static int[] d; // 储存结果
private static int[] coins = { 1, 2, 4, 5 }; // 硬币种类
private static List<List<int>> data;

private static void D_func(int num, int i = 0)
{
    if (i == 0)
    {
        d = new int[num + 1]; // 初始化数组
        data = new List<List<int>> { new List<int> { 0 } };//集合需要的硬币

        D_func(num, i + 1);
    }
    else
    {
        data.Add(new List<int>());
        int min = num + 1; // 初始化一个很大的数值。当最后如果得出的结果是这个数时,说明凑不出来。
        foreach (int coin in coins)
        {
            if (i >= coin && d[i - coin] + 1 < min)
            {
                min = d[i - coin] + 1;
                data[i] = data[i - coin].Concat(new List<int> { coin }).ToList();
            }
        }
        d[i] = min;

        if (i < num)
        {
            D_func(num, i + 1);
        }
    }
}

static void Main(string[] args)
{
    int sum = 17;

    D_func(sum);

    Console.WriteLine("凑齐 " + sum + " 元需要 " + d[sum] + " 个硬币");
    Console.WriteLine("所需硬币为:" + JsonConvert.SerializeObject(data[sum]));//直接用json输出了
    Console.ReadLine();
}

 

针对原方案只给了每种金额所需的数量并没有给出选择的方案,所以我用了一个合集去接每次计算出来的方案,最后只输出了我需要的金额及对应方案。到这里其实第一步已经完成了,接下来就可以在此基础上各种作了 ,比如所需金额太大是不是可以先拆分下?如果没有一元硬币是不是就撂挑子了等等……可玩性还是非常高的具体怎么个耍法就慢慢折腾喽。

 

支持一下:

上述的代码复制下就能跑,但还是偷偷的放个较为完整版链接。如果大佬不惜吝啬给个支持呗,都怪这破站太贵了!

下载地址:较完整的源码(为了方便引用了几个库)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值