1.烤鸡
题目背景
猪猪 Hanke 得到了一只鸡。
题目描述
猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 10 种配料(芥末、孜然等),每种配料可以放 1 到 3 克,任意烤鸡的美味程度为所有配料质量之和。
现在, Hanke 想要知道,如果给你一个美味程度 n ,请输出这 10 种配料的所有搭配方案。
输入格式
一个正整数 n,表示美味程度。
输出格式
第一行,方案总数。
第二行至结束,10个数,表示每种配料所放的质量,按字典序排列。
如果没有符合要求的方法,就只要在第一行输出一个 0。
样例 #1
样例输入 #1
11
样例输出 #1
10 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1
思路与代码
全排列问题,对于每一种调料,我们要选1g,2g或3g
利用dfs(),第一个参数存调料的种类,第二个存美味值总和。
如果这个美味值总和恰好到了所给的n值,且每种调料都用到了,此时即终止搜索。
否则,三种情况继续进入dfs(), 即美味值总和+1, +2 或 +3.
但是,当搜到第10层,如果不符合sum为所给的n值,就要剪枝处理。
#include<bits/stdc++.h> using namespace std; int n, ans = 0; int a[100]; int b[100000][100]; void dfs(int k, int sum) { if(sum > n) return; if(k == 11 && sum == n) { ans++; for(int i = 1; i <= 10; i++) b[ans][i] = a[i]; return; } for(int i = 1; i <= 3; i++) { a[k] = i; dfs(k + 1, sum + a[k]); a[k] = 0; } } int main() { cin >> n; if(n < 10 || n > 30) { cout << 0; return 0; } dfs(1, 0); cout << ans << endl; for(int i = 1; i <= ans; i++) { for(int j = 1; j <= 10; j++) cout << b[i][j] << " "; cout << endl; } }
2.火星人
题目描述
人类终于登上了火星的土地并且见到了神秘的火星人。人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法。这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学家,科学家破解这个数字的含义后,再把一个很小的数字加到这个大数上面,把结果告诉火星人,作为人类的回答。
火星人用一种非常简单的方式来表示数字――掰手指。火星人只有一只手,但这只手上有成千上万的手指,这些手指排成一列,分别编号为 1,2,3,\cdots。火星人的任意两根手指都能随意交换位置,他们就是通过这方法计数的。
一个火星人用一个人类的手演示了如何用手指计数。如果把五根手指――拇指、食指、中指、无名指和小指分别编号为 1,2,3,4 和 5,当它们按正常顺序排列时,形成了 5 位数 12345,当你交换无名指和小指的位置时,会形成 5 位数 12354,当你把五个手指的顺序完全颠倒时,会形成 54321,在所有能够形成的 120 个 5 位数中,12345 最小,它表示 1;12354 第二小,它表示 2;54321 最大,它表示 120。下表展示了只有 3 根手指时能够形成的 6 个 3 位数和它们代表的数字:
三进制数 | 代表的数字 |
---|---|
123 | 1 |
132 | 2 |
213 | 3 |
231 | 4 |
312 | 5 |
321 | 6 |
现在你有幸成为了第一个和火星人交流的地球人。一个火星人会让你看他的手指,科学家会告诉你要加上去的很小的数。你的任务是,把火星人用手指表示的数与科学家告诉你的数相加,并根据相加的结果改变火星人手指的排列顺序。输入数据保证这个结果不会超出火星人手指能表示的范围。
输入格式
共三行。 第一行一个正整数 N,表示火星人手指的数目(1 \le N \le 10000)。 第二行是一个正整数 M,表示要加上去的小整数(1 \le M \le 100)。 下一行是 1 到 N 这 N 个整数的一个排列,用空格隔开,表示火星人手指的排列顺序。
输出格式
N 个整数,表示改变后的火星人手指的排列顺序。每两个相邻的数中间用一个空格分开,不能有多余的空格。
样例 #1
样例输入 #1
5 3 1 2 3 4 5
样例输出 #1
1 2 4 5 3
思路与代码
这也是一种全排列,不过无重复数字。
最开始的想法是,直接全排列出来,然后找到和火星人的数字相同的排列,然后再向后推。
如下是代码,用一个二维数组,把所有的存进去。而且如果这个数访问过了,就不排列了。
不过,看一下数据范围,n <= 10000,方案数一共是(n!)个,如果开二位数组存,肯定会超空间的。
而且时间复杂度也是n!,也会超。只好想办法优化。
void dfs(int k) { if(k == n + 1) { ans++; for(int i = 1; i <= n; i++) d[ans][i] = b[i]; return; } for(int i = 1; i <= n; i++) //!!!!!!!!!! { if(vi[i] == 0) { vi[i] = 1; b[k] = i; dfs(k + 1); vi[i] = 0; } } }
其实,可以从火星人所给的数字开始往后排列,找到第m个,就停下来了。用!注释的代码处,用dis存储方案数,此处修改可以把火星人的数字作为排列的起点,然后dis += 1; //由于for循环是按字典序进行的,所以接下来的排列符合预期的排列。
到了这一步,看似很完美。但实际上,当前的已经找到了,其他的dfs还在继续往后走,还是会导致超时。
void dfs(int k) { if(k == n + 1) { dis++; //!!!!!!!!!! if(dis == m + 1) //!!!!!!!!!! { for(int i = 1; i <= n; i++) cout << b[i] << " "; } return; } for(int i = 1; i <= n; i++) //!!!!!!!!!! { if(dis == 0) //!!!!!!!!!! i = a[k]; //!!!!!!!!!! if(vi[i] == 0) { vi[i] = 1; b[k] = i; dfs(k + 1); vi[i] = 0; b[k] = 0; } } }
函数里的return,只能打断当前函数的进行,不能使整个程序终止,所以用全局变量havefind来说明已经找到了。当找到答案后,将havefind设置为true,从而使得其他正在进行dfs函数终止。
void dfs(int k) { if(havefind) return; //!!!!!!!! if(k == n + 1) { dis++; if(dis == m + 1) { havefind = true; //!!!!!!! for(int i = 1; i <= n; i++) cout << b[i] << " "; } return; } //。。。。。
最后,附上全代码
#include<bits/stdc++.h> using namespace std; int n, m, ans = 0; int a[10002] ,b[10002]; int vi[10001]; int dis = 0; bool havefind = false; void dfs(int k) { if(havefind) return; if(k == n + 1) { dis++; if(dis == m + 1) { havefind = true; for(int i = 1; i <= n; i++) cout << b[i] << " "; } return; } for(int i = 1; i <= n; i++) { if(dis == 0) i = a[k]; if(vi[i] == 0) { vi[i] = 1; b[k] = i; dfs(k + 1); vi[i] = 0; b[k] = 0; } } } int main() { cin >> n >> m; for(int i = 1; i <= n; i++) cin >> a[i]; dfs(1); return 0; }
3.火柴棒等式
题目描述
给你 n 根火柴棍,你可以拼出多少个形如 A+B=C 的等式?等式中的 A、B、C 是用火柴棍拼出的整数(若该数非零,则最高位不能是 0)。用火柴棍拼数字 0\sim9 的拼法如图所示:
注意:
-
加号与等号各自需要两根火柴棍;
-
如果 A\neq B,则 A+B=C 与 B+A=C 视为不同的等式(A,B,C\geq0);
-
n 根火柴棍必须全部用上。
输入格式
一个整数 n(1 \leq n\leq 24)。
输出格式
一个整数,能拼成的不同等式的数目。
样例 #1
样例输入 #1
14
样例输出 #1
2
样例 #2
样例输入 #2
18
样例输出 #2
9
思路与代码
还是全排列,遍历三个数的不同可能,保证两个条件,前两个数相加为第三个数,三个数所用的火柴棒的和为n-4,减去的4为摆加号和等号所需的火柴棒。
此处火柴棒最多给24条,最多是711+0=711这个算式,所以搜索时,要从0搜到1000,一般来说是会超时的,所以需要剪枝。
关于剪枝,需要注意的地方,在于判断条件的优先级。
比如,从 bfs(1, 0)开始,搜完第三个后,将进行bfs(4, sum),第一个参数到了4,当前搜索就可以停止了。
但是第一次写的时候,将 k == 4 与另外两个条件写成并列关系,如果都符合才return,这就导致 k > 4 会一直搜下去,直到sum > n。
另外,存不同数字所需火柴棒时,还需要计算两位数乃至三位数所需的火柴棒。使用递推,把棒棒存到数组里就很方便。
#include<bits/stdc++.h> using namespace std; int n, ans = 0; int dx[10000] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6}; int b[10000]; void dfs(int k, int sum) { if(sum > n) return; //超时减枝 if(k == 4) { if(sum == n && b[1] + b[2] == b[3] ) { ans++; cout << b[1] << " + " << b[2] << " = " << b[3] << endl; } return; } for(int i = 0; i <= 1000; i++) { b[k] = i; dfs(k + 1, sum + dx[i]); } } int main() { cin >> n; n -= 4; for(int i = 10; i <= 1000; i++) dx[i] = dx[i % 10] + dx[i / 10]; dfs(1, 0); cout << ans; }
4.PERKET
题目描述
Perket 是一种流行的美食。为了做好 Perket,厨师必须谨慎选择食材,以在保持传统风味的同时尽可能获得最全面的味道。你有 n 种可支配的配料。对于每一种配料,我们知道它们各自的酸度 s 和苦度 b。当我们添加配料时,总的酸度为每一种配料的酸度总乘积;总的苦度为每一种配料的苦度的总和。
众所周知,美食应该做到口感适中,所以我们希望选取配料,以使得酸度和苦度的绝对差最小。
另外,我们必须添加至少一种配料,因为没有任何食物以水为配料的。
输入格式
第一行一个整数 n,表示可供选用的食材种类数。
接下来 n 行,每行 2 个整数 s_i 和 b_i,表示第 i 种食材的酸度和苦度。
输出格式
一行一个整数,表示可能的总酸度和总苦度的最小绝对差。
样例 #3
样例输入 #3
4 1 7 2 6 3 8 4 9
样例输出 #3
1
提示
数据规模与约定
对于 100\% 的数据,有 1 \leq n \leq 10,且将所有可用食材全部使用产生的总酸度和总苦度小于 1 \times 10^9,酸度和苦度不同时为 1 和 0。
思路与代码
二选一的全排列,对于每种调料,要么加,要么不加,所以还是比较简单
所以不需要回溯,直接向下搜索就是,
如果前n个搜完了,则到了第n + 1个,如果是清水,即没有加任何配料,就剪掉。
不过我第一次剪枝把清水的判断放到了第一行,应该放到 if(k > n)里,还是判断条件优先级的问题。刚开始选配料是,也是清水
然后用x保存最小值,由于求的是差值的绝对值,所以最小值 >= 0, 此处x = -1表示未被搜索过。
#include<bits/stdc++.h> using namespace std; int x = -1, n; int s[10000], b[10000]; void dfs(int k, int sum_s, int sum_b) { if(k > n) { if(sum_s == 1 && sum_b == 0) return; if(x == -1) x = abs(sum_s - sum_b); else x = min( x, abs(sum_s - sum_b) ); return; } dfs(k + 1, sum_s * s[k], sum_b + b[k]); dfs(k + 1, sum_s, sum_b); } int main() { cin >> n; for(int i = 1; i <= n; i++) cin >> s[i] >> b[i]; dfs(1, 1, 0); cout << x; }
5.奇怪的电梯
题目描述
呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯,而且第 i 层楼(1 \le i \le N)上有一个数字 K_i(0 \le K_i \le N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 K_i(K_1=3,K_2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 -2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?
输入格式
共二行。
第一行为三个用空格隔开的正整数,表示 N, A, B(1 \le N \le 200,1 \le A, B \le N)。
第二行为 N 个用空格隔开的非负整数,表示 K_i。
输出格式
一行,即最少按键次数,若无法到达,则输出 -1
。
样例 #1
样例输入 #1
5 1 5 3 3 1 2 5
样例输出 #1
3
思路与代码
本质还是一个无重复数的全排列问题,
对于每一次选择,只能从没走过的电梯选择,因为上下电梯,如果你从某一楼开始,走了几次又绕回来了,说明走重复路了,舍掉。
虽然说是全排列,但是每次搜索可以做的选择是有限的,只有上和下,如果上下可以走到已经标记的路,就不走。
额外的剪枝处理,如果当前按过按钮的次数大于等于最小值,就打断。
对于感叹号注释的一行,最开始我的假想是,如果这一层的电梯移动的数 是0,那么就只能原地转圈,所以把这个也剪掉了,反而错的更多了
原来,是因为下面的 if 已经处理了,如果是0,那么v[t1]和v[t2]都已经访问过了,就不会出现原地转圈的情况。
void dfs(int k, int place) // k 表示次数, place表示楼层位置 { //if(x[place] == 0) return; !!!!!!!! if(ans != -1 && k >= ans) return; if(place == b) { if(ans == -1) ans = k; else ans = min(k, ans); cout << ans; return; } int t1 = place - x[place], t2 = place + x[place]; if( !v[t1] && t1 >= 1) { v[t1] = 1; dfs( k + 1, t1); v[t1] = 0; } if( !v[t2] && t2 <= n) { v[t2] = 1; dfs( k + 1, t2); v[t2] = 0; } }
等学了高级算法以后再说吧