题目描述:
买书
蒜头君去书店买书,他有m元钱,书店里面有n本书,每本书的价格为pi元。蒜头君很爱学习,想把身上钱都用来买书,并且刚好买k本书。请帮蒜头君计算他是否能刚好用m元买k本书。
Input
第一行输入3个整数 m(1≤m≤100000000),n(1≤n≤30),k(1≤k≤min(8,n))
接下来一行输入n个整数,表示每本书的价格 pi(1≤pi≤100000000)。
Output
如果蒜头君能刚好用m元买k本书,输入一行"Yes", 否则输出"No"。
Sample Input 1
10 4 4
1 2 3 4
Sample Output 1
Yes
Sample Input 2
10 4 3
1 2 3 4
Sample Output 2
No
我提交的代码:
#include"iostream"
using namespace std;
int m, n, k,flag = 0,count = 0,f = 0,a[35],vis[35]={0};
// 当前个数 当前的总价
void dfs(int x,int y){
if(x == k && y == m){
flag = 1;
count ++;
return ;
}
if(count == 1) return;
if(x > k || y > m) return ;
for(int i = x;i < n;i ++){
f = 1;
for(int j = 0;j < x;j ++){
if(vis[j] == a[i]){
f = 0;
}
}
if(f){
vis[x] = a[i];
dfs(x+1,y+a[i]);
vis[x] = 0;
}
}
}
int main(){
cin >> m >> n >> k;
for(int i = 0;i < n;i ++){
cin >> a[i];
}
dfs(0,0);
if(flag){
cout << "Yes";
}else{
cout << "No";
}
return 0;
}
这道题尽管通过了,但是我发现我的思维是有问题的,好几个地方都有瑕疵:
1.我的dfs中的x是表示的当前存入的个数,然而这里我却一直用它当作下标来使用,这里个数与下标的混淆导致思路很乱,假设这个i=0,到是还好理解一些,就是始终遍历所有的,从里面选(只不过改为0,是超时的),
然而我这里整了一个x后,尽管减少了一些执行次数,但是理解起来很费劲,因为x毕竟表示的是个数,而且也不太合乎逻辑。(其实我想表达的意思是,之前选过的就不会在去选他了,而是在他的下一个位置继续开始,但是显然我的这种做法是没有达到目的的)
2.另外我的代码其实是将所有的符合条件的可能都找了一遍(尽管说在1.中让i=x,可以减少一些情况的发生,但是还是很多,我要做的其实就是只要让他找到一次就可以了,其余的找到了也没用,只会降低时间效率),我这里整了一个count来记录满足情况的个数(我的代码效率很低,比如说如果钱数为10,个数为4,那么1 2 3 4符合,则1 3 2 4也符合。。。这样就有很多,我整了一个count就是想让它记录一次后,就不再记录后面的了),这里整的勉强,尽管说思路是对的,但是不够好
3.对于dfs()中的形参的选择,一定要考虑周全,考虑得当,否则一开始就将自己引入一个麻烦的境地,有时候甚至可以有减少的思想,不一定用累加的思想去实现(就是说,计数的可以是n–,依次递减的,而不是n++,每递归一次加一的)
革命尚未成功,同志仍需努力!!!
在学长的指导下,我的代码有了很多方面的优化,都是值得我去学习和借鉴的:
#include<bits/stdc++.h>
using namespace std;
int money , n , K; // k = 8
int A[35] , M;
bool succ;
void dfs(int pos , int cont , int tot){
if(tot > money) return;
if(cont == K){
if(tot == money)
succ = true;
return;
}
int rest = K - cont;
for(int i = pos; i <= M - rest; ++i){
dfs(i + 1 , cont + 1 , tot + A[i]);
if(succ)
return;
}
}
int main(){
cin >> money >> n >> K;
M = 0;
for(int i = 0; i < n; ++i){
int t; cin >> t;
if(t <= money)
A[M ++] = t;
}
sort(A , A + M);
reverse(A , A + M);
succ = false;
dfs(0 , 0 , 0);
if(succ) puts("Yes");
else puts("No");
return 0;
}
学习与借鉴:
1.变量的命名很规范,我的显得很随意
2.优化一:对于那些大于money的值,直接忽略了,并且对A数组(即存书价的数组进行了从大到小的排序),这里的从大到小排列很精妙,这能很大的减少dfs的次数,比如:
先看一下从小到大的:
从大到小的:
显然这样要节省很多比较次数
3.通过2.可以看出,每次dfs都是只能在它已走过的数值的右边再去选则,不会选之前选过的,也就是对应我之前的代码,在dfs方法中的循环中,没有让i=0,而是等于的当前的下标pos,之后再去向后dfs
效果就相当于上面那个从小到大的排序的图一样的顺序
4.还有一个地方就是有些分支,可以减支,比如说按照3.中说的这种思想,就是不会再去选之前的了,这样还以那个图为例,最后的分支只有两个了,这就可以直接排除,从而减少了结点数提高了效率。
举个例子还是 1 1 1 9 9 假设买3本,使用dfs遍历,开始以第一个1为第一层,当第二层是倒数第二个数9的时候,第三层还可以选倒数第一个数9,但是当第二层是倒数第一个9的时候,很显然他的后面已经没有可供选择的数了。这样这种情况直接被排除掉。
再比如 1 2 3 4 5 6 7 8 9,在不考虑钱的情况下再去理解,如果我需要凑4个数,那么如果我的第二个数选7,则第三个数只能选8,最后一个数只能选9,如果要是第二个数选8了,显然它后面数怎么凑也凑不够4个数了,所以就直接舍去,其实对应到每一层,都有不够数的,不够数的就直接排出了
rest = 要买的个数 - 当前个数(就是还没买的个数), i <= 数组总个数 - rest
学习使人痛苦,不学习更使人痛苦–舍友名言