Codeup Contest ID:100000608
题目描述
排列与组合是常用的数学方法。
先给一个正整数 ( 1 < = n < = 10 )
例如n=3,所有组合,并且按字典序输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
输入
输入一个整数n( 1<=n<=10)
输出
输出所有全排列
每个全排列一行,相邻两个数用空格隔开(最后一个数后面没有空格)
样例输入
3
样例输出
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
思路
这题第四章的递归里面有详细的代码,我也是参考了那个。主要的DFS过程就是,如果选完了n个数,说明已经形成了一种方案,就输出,然后回退到上一个“岔路口”,体现在代码中就是hashtable[i] = false(把这个数拿掉)。因为“岔路口”的选择是根据之前选的数来的,所以要用一个for循环。
代码
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 11;
bool hashtable[maxn]={0};
int ans[maxn]={0};//存放要输出的结果
void dfs(int index, int n){
if(index==n+1){//如果选完了n个数(因为要从1开始填,一直填到n,如果n+1才退出)
for(int i=1;i<=n;i++){
if(i==1) printf("%d", ans[i]);
else printf(" %d", ans[i]);
}
printf("\n");
return;
}
for(int i=1;i<=n;i++){
if(hashtable[i]==false){
ans[index] = i;
hashtable[i] = true;//说明i已经选中
dfs(index+1, n);//选下一个数
hashtable[i] = false;
}
}
}
int main(){
int n;
while(scanf("%d", &n) != EOF){
dfs(1, n);//从1~n进行全排列
memset(hashtable, 0, sizeof(hashtable));
}
return 0;
}
题目描述
排列与组合是常用的数学方法,其中组合就是从n个元素中抽出r个元素(不分顺序且r < = n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。
现要求你不用递归的方法输出所有组合。
例如n = 5 ,r = 3 ,所有组合为:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
输入
一行两个自然数n、r ( 1 < n < 21,1 < = r < = n )。
输出
所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,所有的组合也按字典顺序。
思路
这题只要在全排列代码的基础上改一下就行了,如果出现问题,请用单步调试并添加关键变量来查看整个递归过程,看看到底哪儿错了,千万不要自己想,我就是在这里卡了好久,因为我一开始在处理下一个数递归函数中写的是dfs(index+1, nowNum+1, n, r),结果就出现了{1,4,3}这种排列的错误,用了单步调试才发现,在处理第三个位置的数时,nowNum始终是3(可以从数值上理解为此时的nowNum和index同步了,index是3,nowNum也是3),而我们期望的是5,因此,下一个位置的数的nowNum应该是当前数的下一个(当前数是i,因为下一个位置的数nowNum应该是i+1),这样才不会出现降序的情况。
代码
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 22;
bool hashtable[maxn]={0};
int ans[maxn]={0};//存放要输出的结果
void dfs(int index, int nowNum, int n, int r){//nowNum记录当前放的数字
if(index==r+1){//如果选完了n个数(因为要从1开始填,一直填到n,如果n+1才退出)
for(int i=1;i<=r;i++){
if(i==1) printf("%d", ans[i]);
else printf(" %d", ans[i]);
}
printf("\n");
return;
}
for(int i=nowNum;i<=n;i++){
if(hashtable[i]==false){
ans[index] = i;
hashtable[i] = true;//说明i已经选中
dfs(index+1, i+1, n, r);//选下一个数,nowNum应该是当前数i的下一个数
hashtable[i] = false;
}
}
}
int main(){
int n, r;
while(scanf("%d%d", &n, &r) != EOF){
dfs(1, 1, n, r);//从第1位的数字1开始排列
memset(hashtable, 0, sizeof(hashtable));
}
return 0;
}
题目描述
已知 n 个整数b1,b2,…,bn
以及一个整数 k(k<n)。
从 n 个整数中任选 k 个整数相加,可分别得到一系列的和。
例如当 n=4,k=3,4 个整数分别为 3,7,12,19 时,可得全部的组合与它们的和为:
3+7+12=22 3+7+19=29 7+12+19=38 3+12+19=34。
现在,要求你计算出和为素数共有多少种。
例如上例,只有一种的和为素数:3+7+19=29。
输入
第一行两个整数:n , k (1<=n<=20,k<n)
第二行n个整数:x1,x2,…,xn (1<=xi<=5000000)
输出
一个整数(满足条件的方案数)。
样例输入
4 3
3 7 12 19
样例输出
1
思路
从n个整数中挑选k个数的思路和上一题类似,找过的数不必再找(数学中的C(n,m),不看重前后顺序),因此递归处理下一个数的时候,需要将i+1传入nowi的值以改变for循环的初始值(从下一位开始循环,比如{3,7,12}和{3,7,19},3和7确定之后,只要从12开始循环以此找到第三个数,其他情况同理)。
那么在“死胡同”里的判断方法就是,如果挑选好了k个数(index==k+1),就进入两个方向的选择,如果是素数,那么方案数cnt++,并清空sum,如果不是素数,那么清空sum之后直接return返回上一层即可。这里要注意的是,我选择了temp数组来暂存选择的数,然后在挑选好数之后,再进行sum,如果在挑选的时候就直接sum += 的话,那么返回上一个“岔路口”的时候,sum值的处理会很麻烦。此外,在dfs里实际的参量只有index和nowi两个量是需要处理的,因为考虑到cnt是要返回的,同时也要读入n和k的值,我这里都用了别名,这样的话,函数处理完之后,cnt就是我们要的结果。
代码
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 21;
int p[maxn]={0};//用于存放输入的数
int temp[maxn]={0};//暂存选中的数
bool hashtable[maxn]={0};
bool is_prime(int x){
if(x<=1) return false;
int sqr = (int)sqrt(1.0*x);
for(int i=2;i<=sqr;i++){
if(x%i==0) return false;
}
return true;
}
void dfs(int &n, int &k, int index, int nowi, int &sum, int &cnt){
if(index==k+1){//如果选完了k个数
for(int i=1;i<=k;i++) sum += temp[i];//计算挑选的k个数的和
if(is_prime(sum)==true){//如果和是素数
sum = 0;//清零
cnt++;
return;
}
else{//如果和不是素数
sum = 0;//清零
return;
}
}
for(int i=nowi;i<=n;i++){
if(hashtable[i]==false){//i号位的数未被选中
temp[index] = p[i];//存到temp里
hashtable[i] = true;
dfs(n, k, index+1, i+1, sum, cnt);
hashtable[i] = false;
}
}
}
int main(){
int n, k;
while(scanf("%d%d", &n, &k) != EOF){
for(int i=1;i<=n;i++) scanf("%d", &p[i]);
int sum, cnt;
sum = cnt = 0;
dfs(n, k, 1, 1, sum, cnt);
printf("%d\n", cnt);
}
return 0;
}
题目描述
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。
输入
一个整数n( 1 < = n < = 10 )
输出
每行输出对应一种方案,按字典序输出所有方案。每种方案顺序输出皇后所在的列号,相邻两数之间用空格隔开。如果一组可行方案都没有,输出“no solute!”
样例输入
4
样例输出
2 4 1 3
3 1 4 2
思路
n皇后的问题在第四章的递归里也提到过了,这题的思路和全排列差不多,只不过要筛去对角线的情况(因为对n个数全排列的话,下标代表行号,n个数代表列号,这就意味着这些棋子一定不在同一行同一列上),在这里我用了回溯的写法,如果遇到已经成为对角线的情况,就直接回退,不再往下递归。
另外需要注意的是,这题要记录方案数,n皇后的问题不是都有解的(比如2皇后就无解),这时就要输出"no solute!",因此我用了cnt作为别名的方式传入函数,这样的话,函数结束之后,cnt就是我们要的方案数,再在主函数判断即可。
代码
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 11;
bool hashtable[maxn] = {0};
int p[maxn] = {0};//i是行号,p[i]是列号
void queen(int index, int n, int &cnt){
if(index==n+1){//如果已经放好了n个数
cnt++;//方案数+1
for(int i=1;i<=n;i++){
if(i==1) printf("%d", p[i]);
else printf(" %d", p[i]);
}
printf("\n");
return;
}
for(int i=1;i<=n;i++){
if(hashtable[i]==false){
bool flag = true;
for(int pre=1;pre<index;pre++){//如果已经发生冲突了,就回溯,不再往后递归
if(abs(index-pre)==abs(i-p[pre])){
//这里不能写p[index]-p[pre],虽然逻辑上没问题,但是此时p[index]还没放东西
flag = false;
break;
}
}
if(flag==true){
p[index] = i;//p[index]等价于i的值,所以上面写i-p[pre]
hashtable[i] = true;
queen(index+1, n, cnt);
hashtable[i] = false;
}
}
}
}
int main(){
int n;
while(scanf("%d", &n) != EOF){
int cnt = 0;
queen(1, n, cnt);//从第1行开始
if(cnt==0) printf("no solute!\n");
memset(hashtable, 0, sizeof(hashtable));
memset(p, 0, sizeof(p));
}
return 0;
}
题目描述
栈是常用的一种数据结构,有n令元素在栈顶端一侧等待进栈,栈顶端另一侧是出栈序列。你已经知道栈的操作有两•种:push和pop,前者是将一个元素进栈,后者是将栈顶元素弹出。现在要使用这两种操作,由一个操作序列可以得到一系列的输出序列。请你编程求出对于给定的n,计算并输出由操作数序列1,2,…,n,经过一系列操作可能得到的输出序列总数。
输入
一个整数n(1<=n<=15)
输出
一个整数,即可能输出序列的总数目。
样例输入
3
样例输出
5
提示
先了解栈的两种基本操作,进栈push就是将元素放入栈顶,栈顶指针上移一位,等待进栈队列也上移一位,出栈pop是将栈顶元素弹出,同时栈顶指针下移一位。
用一个过程采模拟进出栈的过程,可以通过循环加递归来实现回溯:重复这样的过程,如果可以进栈则进一个元素,如果可以出栈则出一个元素。就这样一个一个地试探下去,当出栈元素个数达到n时就计数一次(这也是递归调用结束的条件)。
思路
这题一开始没懂啥意思,以为和全排列一模一样,但是样例给3的时候却只有5个方案。后来发现应该是1~n一定要按这个顺序进栈……稍微列举几种情况就能发现,当进栈的次数和出栈的次数都为n的时候,就获得了一种序列(这是“死胡同”),既然这题也不要求输出序列,就更简单了,直接方案数cnt++即可。
那么本题的“岔路口”在哪呢,很简单,把自身想象成那个数,当你进栈之后,只有两个选择:①出栈;②让下一个数进栈。
于是DFS的代码就出来了~
这题要注意的一点主要是入栈和出栈的判断(“岔路口”该怎么走),如果仅判断left的值,就会造成numPUSH超出n的情况,而导致递归爆栈(一直没有return);如果仅判断numPUSH的值,就会造成left是-1的情况,同样也会导致递归爆栈。所以要是调试的时候直接意外中断的话,多半是有一种情况没有return出来(也就是说,那种情况没有“死胡同”,而是一直走到一个无底洞里去了)。这个时候建议用单步调试看一看参量是不是出错了(我就是这么发现的……-_-||,只能说对堆栈过程的模拟还存在一些细微的逻辑上的错误)。
代码
#include
#include
#include
#include
#include
#include
using namespace std;
void dfs(int numPUSH, int numPOP, int n, int left, int &cnt){
//numPUSH是入栈次数,numPOP是出栈次数,left是剩余元素的个数
if(numPUSH==n&&numPOP==n&&left==0){//如果入栈和出栈次数都为n次,说明完成了一种序列的选择
cnt++;//方案数加1
return;
}
if(numPUSH==0||left==0){//如果入栈次数是0,或者剩余元素是0
dfs(numPUSH+1,numPOP,n,left+1,cnt);//那么只能入栈
}
else if(numPUSH==n||left==n){//如果入栈了n次,或者剩余元素已经满了
dfs(numPUSH,numPOP+1,n,left-1,cnt);//那么只能出栈
}
else{//普通情况
dfs(numPUSH+1,numPOP,n,left+1,cnt);//要么继续入栈,同时剩余元素+1
dfs(numPUSH,numPOP+1,n,left-1,cnt);//要么出栈,同时剩余元素-1
}
}
int main(){
int n;
while(scanf("%d", &n) != EOF){
int cnt = 0;
dfs(0,0,n,0,cnt);
printf("%d\n", cnt);
}
return 0;
}
题目描述
有一个n*m格的迷宫(表示有n行、m列),其中有可走的也有不可走的,如果用1表示可以走,0表示不可以走,文件读入这n*m个数据和起始点、结束点(起始点和结束点都是用两个数据来描述的,分别表示这个点的行号和列号)。现在要你编程找出所有可行的道路,要求所走的路中没有重复的点,走时只能是上下左右四个方向。如果一条路都不可行,则输出相应信息(用-1表示无路)。
请统一用 左上右下的顺序拓展,也就是 (0,-1),(-1,0),(0,1),(1,0)
输入
第一行是两个数n,m( 1 < n , m < 15 ),接下来是m行n列由1和0组成的数据,最后两行是起始点和结束点。
输出
所有可行的路径,描述一个点时用(x,y)的形式,除开始点外,其他的都要用“->”表示方向。
如果没有一条可行的路则输出-1。
样例输入
5 6
1 0 0 1 0 1
1 1 1 1 1 1
0 0 1 1 1 0
1 1 1 1 1 0
1 1 1 0 1 1
1 1
5 6
样例输出
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(2,5)->(3,5)->(3,4)->(3,3)->(4,3)->(4,4)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(2,5)->(3,5)->(3,4)->(4,4)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(2,5)->(3,5)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(3,4)->(3,3)->(4,3)->(4,4)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(3,4)->(3,5)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(3,4)->(4,4)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,4)->(2,4)->(2,5)->(3,5)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,4)->(3,5)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,4)->(4,4)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(4,3)->(4,4)->(3,4)->(2,4)->(2,5)->(3,5)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(4,3)->(4,4)->(3,4)->(3,5)->(4,5)->(5,5)->(5,6)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(4,3)->(4,4)->(4,5)->(5,5)->(5,6)
提示
【算法分析】
用一个a数组来存放迷宫可走的情况,另外用一个数组b来存放哪些点走过了。每个点用两个数字来描述,一个表示行号,另一个表示列号。对于某一个点(x,y),四个可能走的方向的点描述如下表:
2
1 x,y 3
4
对应的位置为:(x, y-1),(x-1, y),(x, y+1),(x+1, y)。所以每个点都要试探四个方向,如果没有走过(数组b相应的点的值为0)且可以走(数组a相应点的值为1)同时不越界,就走过去,再看有没有到达终点,到了终点则输出所走的路,否则继续走下去。
这个查找过程用search来描述如下:
procedure search(x, y, b, p);{x,y表示某一个点,b是已经过的点的情况,p是已走过的路}
begin
for i:=1 to 4 do{分别对4个点进行试探}
begin
先记住当前点的位置,已走过的情况和走过的路;
如果第i个点(x1,y1)可以走,则走过去;
如果已达终点,则输出所走的路径并置有路可走的信息,
否则继续从新的点往下查找search(x1,y1,b1,p1);
end;
end;
有些情况很明显是无解的,如从起点到终点的矩形中有一行或一列都是为0的,明显道路不通,对于这种情况要很快地“剪掉”多余分枝得出结论,这就是搜索里所说的“剪枝”。从起点开始往下的一层层的结点,看起来如同树枝一样,对于其中的“枯枝”——明显无用的节点可以先行“剪掉”,从而提高搜索速度。
思路
思路和提示的差不多,但是我没有完全按照提示中的search描述来写。提示中的小细节提醒已经非常到位了(包括记录走过的路,以及不能越界问题)。
DFS函数里return的条件很简单,就是当前的x、y坐标到达终点的情况(“死胡同”)(因为路不通的话,就根本无法到达四个if函数,就会自动回退到上一个“岔路口”,然后因为走过的路都会标记,也不会无穷无尽地递归下去)。
然后就是普通情况的描写(“岔路口”):把当前的x、y在数组b里面置1,代表现在这个位置已经走过,然后把当前点的坐标放入path数组里(我这里的path数组用了vector容器,容器里面的每一个元素是一对pair(x,y),这正好满足了点的需求,可以理解成path这个容器里放了一堆点。因此,每次到达一个点的时候,就把当前点的坐标放入这个容器里push_back(),如果回退,就把那个点的坐标取出pop_back()),然后用四个if语句按照左上右下的顺序去判断该去哪个岔路口,判断的条件要注意,要那个点有路(a数组中为1),而且那个点没走过(b数组中为0),并且不能越界,要在这张迷宫地图的范围内(1≤x≤n,1≤y≤m)。
如果那个点处理完毕回退回来(不管是到终点回退回来,还是无路可走回退回来),那么就要执行pop_back(),把那个点从path里取出来,然后再把那个点在数组b里置0(←这里的这两个动作没有先后关系哈。如果没有这句话的话,只会输出一条可行的路径,因为采用DFS搜索之后的话,所有点都被标记为走过了,当从终点回退回来的时候,就不会再走任何的点,这里可以参考书上P269的图,走到出口的时候,所有结点都已被遍历,我就是在这里卡了好久-_-||)
然后这个问题就基本上解决了,只不过在使用DFS的时候同样要传入方案数cnt,如果DFS结束之后cnt还是0,就输出无解的情况(即:输出-1)。
另外,我的代码没有做“剪枝”的处理,因为这个二维数组不大,直接写还是能AC的,如果很大的话肯定会超时(因为最坏情况涉及到四个方向的递归,而每个方向的递归又可能会去四个方向进行下一步的递归,即时间复杂度是:O(4n)),那个时候就一定要做“剪枝”处理了,以提升DFS的搜索速度。
代码
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 16;
int a[maxn][maxn];//存放迷宫图
int b[maxn][maxn];//标记已经走过的路
vector<pair<int,int> > path;//存放路径(path里面每一个元素都是pair,代表着一个点)
void dfs(int xs, int ys, int xe, int ye, int x, int y, int n, int m, int &cnt){
//(xs,ys)是起点,(xe,ye)是终点,(x,y)是当前点,n、m是输入的地图大小,cnt是方案数
if(x==xe&&y==ye){
//如果是到达终点的情况,就输出此时的一条路
b[x][y] = 1;//b数组相应位置置1
path.push_back(make_pair(x,y));//把当前点压入path容器里
cnt++;//方案数加1
for(int i=0;i<path.size();i++){
if(i==0) printf("(%d,%d)", path[i].first, path[i].second);
else printf("->(%d,%d)", path[i].first, path[i].second);
}
printf("\n");
return;//返回上一个"岔路口"
}
b[x][y] = 1;//置当前所在点为1(已经走过)
path.push_back(make_pair(x,y));//把当前点压入path容器里
if(a[x][y-1]==1&&b[x][y-1]==0&&x>=1&&x<=n&&y>=1&&y<=m){//如果有路,且没走过,且不越界
dfs(xs, ys, xe, ye, x, y-1, n, m, cnt); //往(x,y-1)这个点走
b[x][y-1] = 0;
path.pop_back();
}
if(a[x-1][y]==1&&b[x-1][y]==0&&x>=1&&x<=n&&y>=1&&y<=m){//如果有路,且没走过,且不越界
dfs(xs, ys, xe, ye, x-1, y, n, m, cnt); //往(x-1,y)这个点走
b[x-1][y] = 0;
path.pop_back();
}
if(a[x][y+1]==1&&b[x][y+1]==0&&x>=1&&x<=n&&y>=1&&y<=m){//如果有路,且没走过,且不越界
dfs(xs, ys, xe, ye, x, y+1, n, m, cnt); //往(x,y+1)这个点走
b[x][y+1] = 0;
path.pop_back();
}
if(a[x+1][y]==1&&b[x+1][y]==0&&x>=1&&x<=n&&y>=1&&y<=m){//如果有路,且没走过,且不越界
dfs(xs, ys, xe, ye, x+1, y, n, m, cnt); //往(x+1,y)这个点走
b[x+1][y] = 0;
path.pop_back();
}
}
int main(){
int n, m;
while(scanf("%d%d", &n, &m) != EOF){
for(int i=0;i<maxn;i++) memset(a[i], 0, sizeof(a[i]));
for(int i=0;i<maxn;i++) memset(b[i], 0, sizeof(b[i]));
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d", &a[i][j]);//读入二维数组
}
}
int x0, y0, x1, y1;
scanf("%d%d", &x0, &y0);//起点
scanf("%d%d", &x1, &y1);//终点
int cnt = 0;
dfs(x0, y0, x1, y1, x0, y0, n, m, cnt);//开始DFS
if(cnt==0){//如果没有一种可行方案
printf("-1\n");
}
for(int i=0;i<maxn;i++) memset(a[i], 0, sizeof(a[i]));
for(int i=0;i<maxn;i++) memset(b[i], 0, sizeof(b[i]));
}
return 0;
}
事实证明代码写得多真的有利于对各种算法的理解(๑•̀ㅂ•́)و✧,我之前对递归一直都读不懂(比如全排列和n皇后问题,第一次看的时候真的是头晕晕乎乎的,更别说DFS了,在做PAT1018.Public Bike Management这道题的时候,完全看不懂各位大佬的DFS代码,虽然知道DFS是“不撞南墙不回头”的一种搜索方式,但还是不会把这种想法转化为实际的代码)。
经过《算法笔记》的讲解(我认为“死胡同”和 “岔路口”这两个概念对于写DFS函数非常有帮助)和DFS这六个题目的练习,对递归和DFS有了更深的理解,同时也深刻感受到,递归的本质就是DFS(当然,仅是我个人认为的哈),因为即便是斐波那契数列的递归实现,其实也是一种DFS(一直到递归边界这个“死胡同”,然后返回上一个“岔路口”),又比如阶乘的递归实现,只不过此时没有所谓的“岔路口”,是一条路到底的情况,到底了之后再逐层返回。
最后,写DFS的函数时候不要先写函数参数,因为你也不知道这个函数里究竟要用到多少参数,那么就先写主体好了,把“死胡同”和“岔路口”两个部分写清楚,最后再把要用到的参数填进去即可。