硬币找零——背包问题,以及循环、递归、动规共通性

在这个题目的基础上,我了解了一下这几个“编程写法”,并对循环、递归、dp有了新的想法。从原理上,这几个想法都是大事化小、小事化了。只不过方向不同罢了。

根据The Algorithm Design Manual,解决这类存在顺序解决关系的问题,我们的通用的想法
在这里插入图片描述
因为递归实际上是一种更自然的思路,从已知到实现只需迈出一步。这一步也抽象了无数步的实现过程。往往更加清晰

实际上我按照这套mindset来考虑dp问题之后,会发现对于并不熟悉的dp题目,明显构思起来要比直接考虑“有什么状态?怎么扩展状态”这套顺手,因为递归是很自然的一种思维方式。当然,已经比较熟练直接就能看出来状态和转移方程的题目,以及一些状态实在太显眼比考虑递归还简单的题目,就不需要用这套了。
PS:有一些优化,使用记忆化搜索方式实现DP做不了,有的题目会被这个卡死,比如必须用滚动数组等方式压缩状态否则MLE的那种。有一些优化,使用刷表法做不了,比如实际访问的状态很稀疏又不能整齐控制顺序,你一建表就MLE或者TLE那种。所以两种DP主流实现方式都要掌握。


作者:sin1080
链接:https://www.zhihu.com/question/323076638/answer/673995021
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

滚动数组绝对是DP的优秀特征,极大概率省去绝大部分的内存,同时通过刷表,将时间复杂度也降到多项式量级;递归的离散搜索也是连续的dp所不能取代的(当然可以用离散化技巧

递归的通解思路

递归有两种主要的形式:搜索-回溯,与分治。
深度优先搜索与回溯法,代表了递归的主要用法之一。传的是层数,适合于排列解空间树的问题
如果我们硬要分别的话,另外还有分治。分治传的是问题规模。分治策略容易估计递归的阶数
其实本质上都是一样的。以后就不必区分了。

void dfs(data x[], int n, int i)
{
     data a[];
     if (i>n) {
         if (x[1~n]是解) {
             输出或保存;
         }
     } else {
          设定子节点a[]: x[i]=a[t];
          for (int t = 0; t <子节点数目; t++) {
               if (legal(x, i, a[t])) {
                  设置第i步现场;
                  dfs(x, n, i+1);
                  恢复第i步现场;
               }
          }
     }
}

当这个题不是分治法思路的时候,我们就应该考虑使用dfs,构建每个元素的幂分支解空间树。也就是像类似这样展开(图题无关)
在这里插入图片描述
对于这个题来说,就是,没有到全部装完的时候,就尝试不同的硬币(显然深搜比起分治也更好理解)。直到产生一系列 a n a^n an树。

如果从分治角度上说,就是可以有几种方法(硬币)回到前一种子情形。然后再对子情形进行递归求解

递归题解

这其实并不是一个讲深搜的好例子,它的最终输出行为是比较复杂的。

如果没有完整方案,要输出剩下钱数最少的找零方案中的最少硬币数
没有搜到的可能性和重要性被放大了。这是一个使用分治策略的深搜(这个表述就很悖论,仍然说明二者是互通的)。

记忆化的分治,记录已经搜索过的部分,是通过离散来填充连续空间

按规模分治。这个方法来自网络,由于使用了两次递归,所以慢的出奇。

int型的穷尽枚举,便于我们大量剪枝。但是实际上,仍然要全部算完才直到某个点处和终点的距离,所以并没有起到良好的剪枝效果。这说明了NPC问题的难解性。

/*
Problem: NYOJ(南阳理工OJ)
Author :2486
Memory: 1012 KB		Time: 192 MS
Language: C/C++		Result: Accepted
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=100000+5;
const int INF=0x3f3f3f3f;
int n,t,a[maxn],vis[maxn],Min;
int dfs(int s) {
    if(s<0)return INF;//非典型的出口:没有找到
    if(vis[s]!=-1)return vis[s]; //必须进行剪枝,不过在标准想法当中,并不是主要步骤
    
    Min=min(s,Min);//最优情况通过‘问题规模’决定,所以只需传递这个问题规模,然后对作为最优解的全局变量进行更新即可。
    int ans=INF;
    for(int i=0; i<n; i++) {
        ans=min(dfs(s-a[i])+1,ans);//对当层进行枚举,其中利用先前结果的想法和完全背包是一样的
    }//得到最少硬币数
    if(ans!=INF)vis[s]=ans;
    return ans;
}
int main() {
    while(~scanf("%d%d",&n,&t),n&&t) {//仅当输出结束时退出
        for(int i=0; i<n; i++) {
            scanf("%d",&a[i]);
        }
        memset(vis,-1,sizeof(vis));
       	vis[0]=0;
        Min=INF;
        dfs(t);
        if(vis[t]==-1) {//是否可以找零,如果不能,就对刚刚已经搜索到的最小差额的情形进行再次递归,这次必将搜索到//这一步是很巧妙的
            dfs(t-Min);
            printf("%d\n",vis[t-Min]);
        } else 
            printf("%d\n",vis[t]);
    }
    return 0;
}

按层深搜。

#include <iostream>
#include <climits>
using namespace std;
int num, t, a[55], ans = INT_MAX, m = INT_MAX; 
// m is the closest record of all slns.

void change(int n, int c) //n is the number of coins, c is the scale of the problem
{
    if (c == m) //at least not worse
        ans = ans < n ? ans : n;
    if (c < m) // a better sln
        ans = n, m = c;
    for (int i = 1; i <= num; i++)
        if (c >= a[i])
            change(n+1, c-a[i]);
}

int main()
{
    cin.sync_with_stdio(false);
    cin >> num >> t;
    for (int i = 1; i <= num; i++)
        cin >> a[i];
    change(0, t);
    cout << ans;
}

动态规划的思考

从dp角度,深搜对应完全背包,j >= w[i]也就对应着剪枝。

不过这二者对维度的枚举顺序并不相同。深搜是在容量的层面上枚举内容,动规是盯着内容单元枚举容量。

这是由算法的不同特征导致的:

  • 动规借助循环实现,所以相对连续,适合于枚举相对连续的容量,这种枚举量大的特点也注定了不能使用递归(当然,记忆化搜索可以解决这个问题)。
  • 而递归本来就可以用来进行相对离散的递推,所以通过枚举内容的单元,可以减少递归调用的发生,这对效率是很有利的。

由于完全背包问题的无限性,可以多次利用先前结果;所以在动规的递推过程当中,是从小到大依次进行的,这和0-1背包显然不同。

这个完全背包的过程可以利用线性数组。

#include <iostream>
#include <cstring>
#define MAXN 100005
using namespace std;
int n, t, a[55], dp[MAXN];
int main()
{
    cin >> n >> t;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    memset(dp, 0x3f, sizeof(dp)); dp[0] = 0;//这个问题中是求最小值,所以应该初始化为较大的3f
    for (int i = 0; i < n; i++)
    for (int j = a[i]; j <= t; j++)
        dp[j] = min(dp[j], dp[j-a[i]] + 1);//可以把输出和动规结合起来。没有太大差异啦
    for (int i = t; i >= 1; i--)
        if (dp[i] != 0)
        {
            cout << dp[i];
            break;
        }
}

这个题目属于典型的NPC问题,使用递归极其缓慢,所以就用动规好了。总体上来说,递归思路很好说明。但是面对这样的问题,深搜只能撂挑子了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值