PAT1 1068 Find More Coins

本文探讨了一个经典的动态规划问题——硬币组合问题,目标是从给定的硬币集合中找到总和为目标值的硬币组合,且要求组合的字典序最小。通过动态规划算法,文章详细解释了如何解决此问题,并提供了Python实现代码。

题目链接
我的github

题目大意

给出 N N N个硬币,要求从这 N N N个硬币中选出若干个硬币使选出的硬币值的和恰好为 M M M

输入

每组包含一个测试用例

  • 第一行是一个两个正数 N ≤ 1 0 4 N\le10^4 N104表示硬币的数量, M ≤ 1 0 2 M\le10^2 M102表示要求的硬币的值的和
  • 第二行是 N N N个正数,表示 N N N个硬币的值

输出

对每个测试用例,升序输出所选的硬币的面值

  • 如果不存在这样一组硬币使硬币的面值和恰好等于 M M M,就输出No Solution
  • 如果存在多组硬币序列,则输出硬币序列的字典最小的那一组

样例输入

8 9
5 9 8 7 2 3 4 1

4 8
7 2 4 3

样例输出

1 3 5

No Solution

解析

这题可用动态规划来解。
实际上这题可以看成是一个01背包的问题,且需要恰好装满。
即背包的容量就是 M M M,每个硬币的重量和面值相等。假设dp[i]表示背包容量是i时,所能装的最大硬币的价值,c[i]表示第i个硬币的价值,则动态转移方程式如下
d p [ i ] = m a x ( d p [ i ] , d p [ i − c [ i ] ] + c [ i ] ) dp[i]=max(dp[i], dp[i-c[i]]+c[i]) dp[i]=max(dp[i],dp[ic[i]]+c[i])
最后判断是否有解时,只需判断dp[m]是否等于m即可。
但是对于有多解的情况仅仅凭上面的几个数组是无法求出字典序最小的那一组。如何改进呢?

  • 先将硬币的值降序排列
  • 加一个flag[i][j]表示第i个硬币,容量是j的时候是否被选中,状态转移的方程式也要改一下
    for i in range(n):
        for j in range(m, 0, -1):
            if j >= c[i] and dp[j] <= dp[j - c[i]] + c[i]:
                dp[j] = dp[j - c[i]] + c[i]
                flag[i][j] = True

即当dp[j]==dp[j - c[i]] + c[i]时也要更新。这是因为我们之前将硬币降序排列了,如果在一个序列里面添加一个比所有元素都小的元素,那么这个序列会变得更小。
同时flag[i][j] = True记录是否选中第i个硬币,方便之后输出字典序最小的硬币组合

输出字典序最小的硬币组合的过程实际上就是逆着找的过程,这样就既保证了硬币面值的升序输出,又保证了硬币组合的字典序最小

# -*- coding: utf-8 -*- 
# @Time : 2019/6/27 16:51 
# @Author : ValarMorghulis 
# @File : 1068.py
def solve():
    n, m = map(int, input().split())
    c = list(map(int, input().split()))
    dp = [0 for i in range(m + 1)]
    flag = [[False for i in range(m + 1)] for j in range(n)]
    c.sort(key=lambda x: -x)
    for i in range(n):
        for j in range(m, 0, -1):
            if j >= c[i] and dp[j] <= dp[j - c[i]] + c[i]:
                dp[j] = dp[j - c[i]] + c[i]
                flag[i][j] = True
    if dp[m] != m:
        print("No Solution")
    else:
        i, j = n-1, m
        while i != -1 and j != 0:
            if flag[i][j]:
                j -= c[i]
                print(c[i], end=('\n' if j == 0 else ' '))
            i -= 1


if __name__ == "__main__":
    solve()

# 代码概述 该程序旨在从一组硬币中找出一个重量不同的“不合格”硬币。假设其他所有硬币重量相同,仅有一个硬币重量不同(可能更重或更轻),通过递归分治法将硬币分成三组,比较前两组的总重量,从而缩小搜索范围。 # 代码解析 ```c #include <stdio.h> ``` 包含标准输入输出库,用于 `printf` 和 `scanf`。 ```c int find(int coins[], int l, int r) { if(l == r) return 1; ``` **注释**:如果左右边界相等,说明只剩一个硬币,即为不合格硬币,返回其位置(注意此处逻辑错误,应返回 `l` 而非固定 `1`)。 ```c int n = r - l + 1; int g = n / 3; int m1 = l + g - 1; int m2 = m1 + g; ``` **注释**:计算当前区间的长度 `n`,每组大致分为 `g = n/3` 的大小;`m1` 是第一组末尾下标,`m2` 是第二组末尾下标。 ```c if(n % 3 == 1){ m1++; } else if(n % 3 == 2){ m1++; m2++; } ``` **注释**:根据余数调整分组边界,使三组尽可能均分。若余1,则第一组多一个;若余2,则前两组各多一个。 ```c int s1 = 0, s2 = 0; for(int i = 1; i <= m1; i++) s1 += coins[i]; for(int i = m1 + 1; i <= m2; i++) s2 += coins[i]; ``` **注释**:计算前两组硬币的总重量。**存在严重错误**:循环从 `i=1` 开始而非 `l`,导致索引错乱,应使用 `l` 到 `m1` 的范围。 ```c if(s1 > s2) { return find(coins, 1, m1); } else if(s2 > s1) { return find(coins, m1 + 1, m2); } else{ return find(coins, m2 + 1, r); } ``` **注释**:比较两组重量。若 $s1 > s2$,则不合格硬币在第一组;若 $s2 > s1$,则在第二组;否则在第三组。但递归调用时传入了错误的区间(如 `1` 而非 `l`)。 ```c int main(){ int n; printf("硬币总数:"); scanf("%d", &n); if(n <= 0){ printf("无效输入\n"); return 1; } int c[1000]; printf("输入每个硬币重量:"); for(int i = 0; i < n; i++) { scanf("%d", &c[i]); } int idx = find(c, 0, n - 1); printf("不合格硬币在第 %d 个位置,重量为 %d \n", idx + 1, c[idx]); return 0; } ``` **注释**:主函数读取硬币数量和重量,调用 `find` 查找异常硬币并输出结果。但 `find` 函数中的索引严重错误,导致行为未定义。 --- # 知识点 - **分治算法**:将问题划分为子问题递归求解。本题尝试三路分治查找异常元素。 - **数组索引与边界处理**:正确使用数组下标是关键,本代码中多处越界访问导致错误。 - **递归终止条件设计**:递归必须有正确的基础情形,且参数传递需一致,否则无法收敛。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值