1068 Find More Coins

Eva loves to collect coins from all over the universe, including some other planets like Mars. One day she visited a universal shopping mall which could accept all kinds of coins as payments. However, there was a special requirement of the payment: for each bill, she must pay the exact amount. Since she has as many as 104 coins with her, she definitely needs your help. You are supposed to tell her, for any given amount of money, whether or not she can find some coins to pay for it.

Input Specification:

Each input file contains one test case. For each case, the first line contains 2 positive numbers: N (≤104, the total number of coins) and M (≤102, the amount of money Eva has to pay). The second line contains N face values of the coins, which are all positive numbers. All the numbers in a line are separated by a space.

Output Specification:

For each test case, print in one line the face values V1​≤V2​≤⋯≤Vk​ such that V1​+V2​+⋯+Vk​=M. All the numbers must be separated by a space, and there must be no extra space at the end of the line. If such a solution is not unique, output the smallest sequence. If there is no solution, output "No Solution" instead.

Note: sequence {A[1], A[2], ...} is said to be "smaller" than sequence {B[1], B[2], ...} if there exists k≥1 such that A[i]=B[i] for all i<k, and A[k] < B[k].

Sample Input 1:

8 9
5 9 8 7 2 3 4 1

Sample Output 1:

1 3 5

Sample Input 2:

4 8
7 2 4 3

Sample Output 2:

No Solution

 翻译:

伊娃喜欢收集宇宙各地的硬币,包括火星等其他行星。有一天,她参观了一家可以接受各种硬币支付的环球购物中心。不过,付款有一个特殊的要求:每张账单,她都必须支付准确的金额。由于她身上有多达 10 4 个硬币,她肯定需要你的帮助。你应该告诉她,对于任何给定的金额,她是否能找到一些硬币来支付。

输入规范:每个输入文件包含一个测试用例。对于每种情况,第一行包含 2 个正数:N(≤10 4 ,硬币总数)和 M(≤10 2 ,Eva 必须支付的金额)。第二行包含N个硬币的面值,均为正数。一行中的所有数字均以空格分隔。

输出规范:对于每个测试用例,在一行中打印面值 V 1 ​ ≤V 2 ​ ≤⋯≤V k ​,使得 V 1 ​ +V 2 ​ +⋯+V k ​ =M。所有数字必须用空格分隔,并且行尾不能有多余的空格。如果这样的解不唯一,则输出最小的序列。如果无解,则输出“No Solution”。

注意:如果存在 k≥1,则序列 {A[1], A[2], ...} 被称为比序列 {B[1], B[2], ...} “小”对于所有 i<k,A[i]=B[i],且 A[k] < B[k]。

样本输入 1:8 9 5 9 8 7 2 3 4 1

 样本输出 1:1 3 5

样本输入 2:4 8 7 2 4 3

样本输出 2:无解


 思考:

优先是遍历

从最小数开始,一旦超过就跳转到下一个数,如此循环

这里最小的定义是所有的数都尽可能的小,优先是最前面的数要最小

也就是要遍历完所有的数才行

小的数都要绑在一起,数字可以多,但面值一定是最小的情况

首先从小面值一直加到目标,理想是最小的数一直往前加直接就等于答案

如果超过,我们就减少前面一个数去和后面的数继续比对,超过就再减继续比对

但是2,3,3,4诸如这些组合的情况呢?怎么讨论?不会了

dfs的方法完美的解决了这个问题,

如1,2,3,4超过以后删除

1,2,3,5

...

后面如此循环,没有答案,就删除3,

变为1,2,4

dfs就是可以继承上一次的结果使用,不满足就一直返回即可

核心也是遍历的思想。后面发现1,2,不满足就变为1,3,后一直往下遍历

至于2,3,4那些情况也是,一旦发现一开始的1也不可行就会开始从2开始向下遍历

因为题意要求就是所有的数尽可能的小,所以使用dfs一直深度遍历下去最贴合题意

关键就是事实也是如此,最小的情况就是数列是连续的,尽可能连续就是最小的结果

满足前面的数优先最小的原则然后依次推算出最最佳的结果!dfs就是最合适的!

因为没有一种遍历能够比dfs这种像树枝一样的延申更适合数优先最小!

因为dfs要延申,那就必须是所有数最小的情况才可能延申出更多的分支!

这种延申本身就包含了前面的数最小的原则!


算法一:背包问题使用dfs

 第一次未能AC的结果

#define _CRT_SECURE_NO_WARNINGS

#include<cstdio>
#include<vector>
#include<algorithm>
#include<unordered_set>
using namespace std;
int n, price;
int coins[10001];
vector<int> res;

bool dfs(int remain, int index) {
	if (remain == 0)
		return true;
	if (remain < 0 || index > n)
		return false;

	res.push_back(coins[index]);
	if (dfs(remain - coins[index], index + 1))
		return true;//继续执行下一步的意思
	res.pop_back();//当当前数字超过remain时,就删除res中最后一个硬币
	//再重新上传下一个数字继续判断是否符合remain
	//不过,这个有不必要的判断,如1234,中4已经不可以了,那后面的数自然也是不可以的
	// 到时看看能不能优化
	// 得了,5ms,不优化也罢,只要代码过得去,能不优化就不优化
	// 你优化首先代码会更多,其次不好理解不简洁,而且可能会有bug
	
	//你思路是正确的就是这么判断,只是你不会算法
	return dfs(remain, index + 1);
}
int main() {
	scanf("%d %d", &n, &price);
	for (int i = 0; i < n; i++) {
		scanf("%d", &coins[i]);
	}
	sort(coins, coins + n);
	dfs(price, 0);
	if (res.empty()) {
		printf("No Solution");
		return 0;
	}
	for (int i = 0; i < res.size(); i++) {
		if (i != 0)
			printf(" ");
		printf("%d", res[i]);
	}
}



对于大量数据,就要考虑提升算法效率使用visited数组标记


AC
 

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<map>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;

int n, price;
int coins[10010];
bool visited[110][10010] = { false };
vector<int>res;

bool dfs(int remain, int index)
{
	if (remain == 0)
		return true;
	if (remain<0 || index>n || visited[remain][index])
		return false;

	visited[remain][index] = true;
	res.push_back(coins[index]);
	if (dfs(remain - coins[index], index + 1))
		return true;

	res.pop_back();
	dfs(remain, index + 1);

}

int main()
{
	
	cin >> n >> price;
	

	for (int i = 0; i < n; i++)
	{
		cin >> coins[i];
	}
	
	sort(coins, coins + n);
	dfs(price, 0);
	if (res.empty())
		//if (res.empty())的意思是res为空时!
		//if (!res.empty())是不为空时!
		//不要混淆!
	{
		cout << "No Solution" << endl;
		return 0;
	}

	for (int i = 0; i < res.size(); i++)
	{
		cout << res[i];
		if (i != res.size() - 1)
			cout << " ";
	}


	return 0;
}

详解:

//return ture会继续执行dfs,一旦return false就会直接结束当前这一次的dfs
//为什么visited一定要用?
//首先,不用visited是可以的,但是对于特殊的庞大数据,
// 有些本可以处理的重复数据能够快速实现的就因为你的算法不行就导致过不了
//其实,这算是题目的一个额外考点。
// 就对于数据庞大来说,本就应该超时,一般情况下是的。一般情况下,无序的数字,根本就不可能
//有这个规律,可以用的上visited。

//visited可以实现快速将大量数据中,如果是重复的数字造成的重复情况你能否处理
//不过也确实,数据中难免会有重复数字,
// 因此有些情况是相同的就不需要再往下遍历了,一眼就是错的,
//大大优化了算法效率和时间,很多数据都不可能是完全不重复的,
// 所以确实不使用visited是不明智的选择。
//什么都重新算一遍是很愚蠢的
//而标记的重复关键就在于remain和index两者完全一致!
//如11234
//不过最后这个数据庞大的测试点才占一分,也就说明比较难思考到吧,不是重点
//但确实是会很大程度上优化算法!

//如在11234中,你一开始的1,和第二个1
//再对于后面的只选中2,3,4的结果都是一样的,
// 这就重复遍历了结果,而且还不算少,自然dfs做了很多重复无意义的工作
//而这个起点就在于大家都是积累了1的值
// 即remain-1及下标从2开始的情况,所以就是此刻之前的标记就起到作用了
//我们在第一个1的时候就已经出现了这种情况,
// 也就是说11234中的第二个1的遍历结果直接跳过就好了,
// 因为跟第一个1仅对于2,3,4的结果是一样的。
//
//依此类推,会有很多类似这样的数据会导致重复遍历的可能,
// 所以标记走过的当前的情况是可大大提高算法的效率的
//
//而标记当前情况就要确定唯一性,而这个唯一性就恰好同时可由remain和index决定!
//(因为无论什么情况,只要是当前位置的remain值之前出现过,那说明前面的数肯定是一样的
//前面的数一样就表示肯定走过!)


#define _CRT_SECURE_NO_WARNINGS

#include<cstdio>
#include<vector>
#include<algorithm>
#include<unordered_set>
using namespace std;
int n, price;
int coins[10001];
bool visited[102][10002];
vector<int> res;


bool dfs(int remain, int index) {
	if (remain == 0)
		return true;
	if (remain < 0 || index > n || visited[remain][index])
		return false;
	visited[remain][index] = true;
	res.push_back(coins[index]);
	if (dfs(remain - coins[index], index + 1))
		return true;//继续执行下一步的意思
	//当dfs中出现了false的时候,判断就会返回,然后执行return true,
	//也就可以执行后面的操作--删除一个数字的操作了
	//如果没有出现false,dfs是不会返回的,也就不会执行后面的语句了

	res.pop_back();//当当前数字超过remain时,就删除res中最后一个硬币
	//再重新上传下一个数字继续判断是否符合remain
	//不过,这个有不必要的判断,如1234,中4已经不可以了,那后面的数自然也是不可以的
	// 到时看看能不能优化
	// 得了,5ms,不优化也罢,只要代码过得去,能不优化就不优化
	// 你优化首先代码会更多,其次不好理解不简洁,而且可能会有bug
	//你思路是正确的就是这么判断,只是你不会算法
	return dfs(remain, index + 1);
}
int main() {
	scanf("%d %d", &n, &price);
	for (int i = 0; i < n; i++) {
		scanf("%d", &coins[i]);
	}
	sort(coins, coins + n);
	dfs(price, 0);
	if (res.empty()) {
		printf("No Solution");
		return 0;
	}
	for (int i = 0; i < res.size(); i++) {
		if (i != 0)
			printf(" ");
		printf("%d", res[i]);
	}
}


方法二:

背包算法

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<map>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;

int coins[10010];
//bool visited[10010][110];  err
//这里跟visited还是有区别的
//selected是选择,是可以覆盖的,
//而visited是标记,防止覆盖!
bool selected[10010][110];
int bags[101];

bool cmp(const int& a, const int& b)//是&不是*,int&不是&int!,跟*写法一样的!
{
	return a > b;
}

int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> coins[i];

	//为了增加程序的稳定性,准确性和可读性,还是改成1开始吧
	//1开始,金额硬币最小只有1,空间也是从1开始
	//你不对齐,就不够准确,在始端和末端的结果就可能被吞了和直接覆盖了结果,就会导致数据不准确
	//意思就是无法准确判断程序运行流程,肯定是有结果没有正确输出!
	//所以数据一定要保证可读性,还有思路的准确性,少写硬币有0元开始的这种极差可读性的代码!
	sort(coins+1, coins + n+1, cmp);

	for(int i=1;i<=n;i++)
		for (int j = m; j >=1; j--)
		{
			if (j - coins[i] >= 0 && bags[j - coins[i]] + coins[i] >= bags[j])//&& visited[i][j] == false)err
				//是选择不是标记,在选择的过程中我们要选出最佳的答案,而不是唯一的,所以你不能直接标记了就不更换了!
				//问题就是我们是要选出最小的硬币组合,所以选择的过程中可能包含更换硬币组合的可能,所以不是
				//不是visited的标记唯一答案,而是selected的选择不同组合!
				
				//所以,是不需要标记了出现过就不用执行了,selected肯定会重复标记true,重复标记选择,只是为了更换bags即可
			{
				selected[i][j] = true;
				bags[j] = bags[j - coins[i]] + coins[i];
			}
		}

	if (bags[m] != m)
	{
		cout << "No Solution";
		return 0;
	}

	int i = n,j = m;

	//while (j != i) err
	while(i&&j)//i与j任意一个真值,意味着如果金额足够了就直接停止循环,或者硬币选完尚未得出结果直接结束
	{
		if (selected[i][j])
		{
			cout << coins[i];
			j -= coins[i];

			if (j != 0)//输出完最后一个硬币时j==0,最后不输出空格
			{
              cout << " ";
			}//这串代码要放在selected里面,必须是输出了结果才输出空格!
			
		
		}
		

		i--;
	}



	return 0;
}

补充解析

#include <iostream>
#include <algorithm>

using namespace std;                        //背包问题要多练

int bags[101];
bool selected[10001][101];

bool cmp(const int& a, const int& b) {
    return a > b;
}

int main() {
    int coins[10001];
    int n, m;// n coins, pay m
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> coins[i];
    }
    sort(coins + 1, coins + 1 + n, cmp);
    //排序从大到小,实现从大的硬币开始排起,然后实现寻找出最小的组合,方便更换
//背包的空间也跟随匹配,先装入,再调整被装入硬币的背包。

    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= 1; j--) { //之所以要反着来,和背包问题的更新规则有关
            //可以顺序,只不过可能反过来的条件题设更好理解一点
            
            //反过来可能是因为是要遍历最小顺序,因为我们先要保证有这种情况
            //然后再保证选出最小的结果,也就是装进背包
            //所以要使用倒序,我们是选出最小的情况
            //等号就是为了更新最小硬币的组合
            if (j - coins[i] >= 0 && bags[j - coins[i]] + coins[i] >= bags[j]) { //等号必须取到,否则输出的解是最大的sequence
                //规定上限,也就是最终结果只能为多大 一开始的m为最大,已经注定了空间一定不会超过m
                // 而我后面一直取>=的值,最终最大的结果也只能是m,因为m最大,>就是为了取无限逼近m的值,而=就是为了与m的结果匹配
                // 同时因为i的for循环而选出最小顺序的结果,既然是m最大,那么理应bag[m]的结果最大,如果<,那就一定不存在!
                //    //后面的是为了更换,也就是换成最小顺序
                //先保证硬币可以装进背包,也就是用下标代替,下标大小就表示空间大小

                selected[i][j] = true; //跟踪哪个物品被选择了
                bags[j] = bags[j - coins[i]] + coins[i];//更新背包大小的步骤,一直往m接近。
            }
        }
    }
    //bags一开始只是拥有空间,但并没有装入硬币,注意。[]是空间,bags的值是支付金额。

    if (bags[m] != m) //【m】代表背包空间,当然是在空间中满足等于m才可输出结果,否则就等于无

        //背包的作用是将硬币装满至m为止,但无法分辨出分别装了什么硬币,只是在装硬币的过程中用selected
        //来标记硬币,最后用其输出coin
    
    {
        cout << "No Solution" << endl;
        return 0;
    }
    //?
    // 
    //下面是输出最优组合的过程,其实和背包问题的更新规则有关,就是沿着选出解的路径,反着走回去,就找到了所有被选择的数字。
    int j = m, i = n;
    //需支付的金额 优先输出最小的金币,所以从n开始
    while (j && i) {
        if (selected[i][j] == true)
        //体现出selected的作用。不使用就没法查找出准确对应的j,i值
           // i的大小让循环慢慢选择出合适的硬币,j是选择出价钱总和
        {
            cout << coins[i];
            j -= coins[i];//总价钱的记录,当j与i一方为0时,也就是没有硬币可以选择或者总价钱为0为终结--停止循环
            if (j != 0)
                cout << " ";
        }
        i--;
    }
    cout << endl;
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值