专题:DFS(基础)
一、基础知识
1. 引入
搜索与回溯是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,可以利用搜索与回溯的技术求解。回溯是搜索算法中的一种控制策略。
2. 基本思想
(1)为了求得问题的解,先选择某一种可能情况向前探索;
(2)在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索;
(3)如此反复进行,直至得到解或证明无解。
举例:迷宫问题:进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其它方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其它方向是否还有路可走;如果有路可走,则沿该方向再向前试探。按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。
二、典例
1. 素数环
给定一个N,求1——N组成的环,使得环上相邻的元素和为素数。
输入描述:
一个整数N(1<=N<=10)
输出描述:
把1放在第一位置,按照字典顺序不重复地输出所有解(顺时针,逆时针算不同的两种),相邻两数之间严格用一个空格隔开,每一行的末尾不能有多余的空格。如果无解,则输出“no”。
样例输入:
8
样例输出:
1 2 3 8 5 6 7 4
1 2 5 8 3 4 7 6
1 4 7 6 5 8 3 2
1 6 7 4 3 8 5 2
【分析】环中只确定起点为1,而其它位置上的数不确定,也就是说有多种可能的组合。遇到此种情境时要考虑搜索+回溯求解。
在这里,除去环的起点,每个位置上的数有N种可能。只要满足以下3点:
(1)填进去的数合法(2~N)
(2)与前面的数不相同;
(3)与左边相邻的数的和是一个素数,特别注意环的终点与环的起点上的数之和也是一个素数。
就可确定所求的环是素数环。
过程如下:
(1)使用两个数组ring和vis。其中ring保存所求素数环中的各个数,vis记录1~N 这N个数是否已使用(因为要保证不重复)
(2)数据初始化;
(3)递归填数:判断第i个数填入是否合法;
①合法:填数,并判断是否已经到达环的终点。如果到达终点,打印结果;否则,继续填下一个数;
②不合法:选择下一种可能。
#include <stdio.h>
#include <math.h>
#include <string.h>
#define maxn 15
int N;
int ans=0; //ans记录解的数目,ans=0时表示无解,输出"no"
int ring[maxn]; //ring数组保存素数环中的数
int vis[maxn]; //vis记录1~N 这N个数是否已填入素数环
int is_Prime(int n) //判素数
{
int i;
if(n<=1) //负数.0和1都不是素数
return 0;
for(i=2;i<=sqrt(n);i++)
{
if(n%i==0)
return 0;
}
return 1;
}
void dfs(int cur) //从第2个数开始,向后搜索符合条件的素数环
{
int i;
if(cur==N) //搜索结束
{
if(is_Prime(ring[0]+ring[cur-1])) //判断素数环首尾数之和是否为素数,如果是,则找到解
{
for(i=0;i<cur-1;i++)
printf("%d ",ring[i]);
printf("%d\n",ring[cur-1]);
ans++;
}
return;
}
for(i=2;i<=N;i++) //试探2~N中的每个数
{
//当i未被使用且i与环中最后一个数之和为素数时,将其填入素数环
if(!vis[i] && is_Prime(ring[cur-1]+i))
{
vis[i]=1; //置已使用标记
ring[cur]=i;//填充
dfs(cur+1); //继续搜索
vis[i]=0; //回溯-清除已使用标记
}
}
}
int main()
{
scanf("%d",&N);
memset(vis,0,sizeof(vis));
if(N==1) //N为1时,因为1不是素数,所以可直接判断无解
printf("no\n");
else //其它情况:搜索
{
ring[0]=1; //先将1填入,作为素数环的起点
dfs(1); //从环的第2个数开始搜素
if(ans==0) //搜索结束后,如果无解,输出"no"
printf("no\n");
}
return 0;
}
2. 排列组合问题
(Ⅰ)设有n个整数的集合{1,2,…,n},从中取出任意r个数进行排列(1<=r<n<=10),试列出所有的排列。
【分析】元素的排列问题
总体思路同上,这里某个元素按不同次序出现的组合应视为不同的排列。例如:1 2 3和2 1 3,元素均为1.2.3,只是排列顺序不同,因此应视为元素1.2.3的不同排列。
特别地,当n=r时,称为n的全排列。实现时只需把下面程序的终点改为cur==n即可。
#include <stdio.h>
#define maxn 12
int n,r;
int ans=0; //符合要求的排列总数
int ret[maxn]; //保存产生的排列
int vis[maxn]; //记录1~n是否已使用 1-是 0-否
void dfs(int cur) //从{1,2,...,n}中取r个数构成的排列
{
int i;
if(cur==r) //r个数已经选完,输出选择方案
{
for(i=0;i<cur-1;i++)
printf("%d ",ret[i]);
printf("%d\n",ret[cur-1]);
ans++;
return;
}
//试探1~n n个数
for(i=1;i<=n;i++)
{
if(!vis[i]) //i未被使用
{
vis[i]=1; //置已使用标记
ret[cur]=i; //将i填入ret[cur]
dfs(cur+1); //继续搜索
vis[i]=0; //回溯:清除标记
}
}
}
int main()
{
scanf("%d %d",&n,&r);
dfs(0);
printf("count=%d\n",ans);
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)。
【输出】
所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。
【样例输入】
5 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
【分析】元素的排列问题
该问题是上述(Ⅰ)的变种。需要注意的是,这里某种数字组合的多种排列视为相同情况,因此需“去重”。
一种可行的方案是:填数的时候
(1)如果当前填的是第一个数,则直接填入;
(2)在(1)的基础上,后面填入的数都要比前面的数大,因此要进行大小的比较。如果不符合条件,则不能填入。这样既能保证每种组合中数是递增的,也能保证组合是按字典序输出的。
#include <stdio.h>
#include <string.h>
#define maxn 22
int n,r;
int a[maxn],vis[maxn];
void dfs(int cur)
{
int i;
if(cur==r)
{
for(i=0;i<cur;i++)
printf("%3d",a[i]);
printf("\n");
return;
}
for(i=1;i<=n;i++)
{
//每个组合中,元素递增排列,且组合按字典序递增排列
if((cur==0) || (cur>0 && i>a[cur-1]))
{
vis[i]=1;
a[cur]=i;
dfs(cur+1);
vis[i]=0;
}
}
}
int main()
{
scanf("%d %d",&n,&r);
memset(vis,0,sizeof(vis));
dfs(0);
return 0;
}
(Ⅲ)有重复元素的排列问题
【问题描述】
设R={ r1, r2 , …, rn}是要进行排列的n个元素。其中元素r1, r2 , …, rn可能相同。试设计一个算法,列出R的所有不同排列。
【编程任务】
给定n 以及待排列的n 个元素。计算出这n 个元素的所有不同排列。
【输入格式】
由perm.in输入数据。文件的第1 行是元素个数n,1≤n≤500。接下来的1 行是待排列的n个元素。
【输出格式】
计算出的n个元素的所有不同排列输出到文件perm.out中。文件最后1行中的数是排列总数。
【输入样例】
4
aacc
【输出样例】(多解)
aacc
acac
acca
caac
caca
ccaa
6
【分析】元素排列+去重
举例:aacc四个元素的全排列
(1)我们可以划分为3个元素的全排列,3个划分为2个,到最后只剩下1个元素,就不需要排列。
(2)我们让每一个元素作为打头元素,交换,然后进行递归,再交换。
(3)如果该打头元素在前面中已经有过,则忽略这种情况。
#include <stdio.h>
#include <string.h>
#define maxlen 510
int n;
int ans=0; //符合条件的排列总数
char line[maxlen]; //读入的字符串
void swap(char *a,char *b)
{
char t;
t=*a;
*a=*b;
*b=t;
}
int has_Same(char *list,int k,int m) //判断list[m]是否在list[k,m-1]中出现过
{
int i;
for(i=k;i<m;i++)
{
if(list[m]==list[i])
return 1;
}
return 0;
}
void perm(char *list,int k,int m) //求排列
{
int i;
//搜索终点:只剩下一个元素待排列
if(k==m)
{
ans++;
for(i=0;i<=m;i++)
printf("%c",list[i]);
printf("\n");
return;
}
for(i=k;i<=m;i++)
{
//去除已经出现过的排列
if(has_Same(list,k,i))
continue;
swap(&list[k],&list[i]);
perm(list,k+1,m);
swap(&list[k],&list[i]);
}
}
int main()
{
int i;
scanf("%d",&n);
scanf("%s",line);
perm(line,0,n-1);
printf("%d\n",ans);
return 0;
}
3. n皇后问题
(Ⅰ)在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。该问题等价于在n×n的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。
输入描述:
给定棋盘的大小n (1 ≤ n ≤ 13)
输出描述
一个整数,表示放置方法的数目。
样例输入:
8
样例输出:
92
【分析】问题的关键在于如何判定某个皇后所在的行、列、斜线上是否有别的皇后;可以从矩阵的特点上找到规律,如果在同一行,则行号相同;如果在同一列上,则列号相同;如果同在/斜线上的行列值之和相同;如果同在\斜线上的行列值之差相同;从下图可验证:
法一:在摆放皇后时,可以”按行摆放”(这样就保证了皇后不会横向攻击)。即:
(1)起点为dfs(0),即从第0行开始摆放皇后,逐行进行。同时使用一维数组map保存第cur行的皇后摆放的列,也就是说每次尝试摆放皇后的位置坐标为(cur, map[cur]);
(2)逐列遍历,若发现位置(i, map[j])与位置(cur, map[cur])在同一列 或 同一主对角线 或 同一副对角线上时,摆放失败,该方案”作废”,继续执行(2);
(3)若摆放成功,则dfs(cur+1),表示继续摆放下一行,过程同上。
(4)当cur=n,即n行皇后均摆放完成时,表示该方案可行,总方案数+1。
#include <stdio.h>
#include <string.h>
#define maxn 20
int n;
int ans=0;
int map[maxn]; //map[i]用于记录第cur行摆放的皇后的列号
void dfs(int cur)//第cur行摆放皇后
{
int i,j;
int suc; //suc用于记录皇后摆放是否成功 1-是 0-否
if(cur==n) //搜索完成,摆放成功
{
//输出n个皇后所在列的列号
/*for(i=0;i<cur;i++)
printf("%d ",map[i]);
printf("\n");*/
ans++;
return;
}
for(j=0;j<n;j++) //逐列遍历
{
suc=1;
map[cur]=j; //尝试把第cur行的皇后放置在第j列,即位置(cur,map[cur])
for(i=0;i<cur;i++) //遍历前cur行,检查上一步的尝试方案是否会产生冲突
{
//皇后(cur,map[cur])和(i,map[i])在同列 或 在同主对角线 或 在同副对角线上时,方案不可行(即摆放失败)
if(map[cur]==map[i] || cur-map[cur]==i-map[i] || cur+map[cur]==i+map[i])
{
suc=0;
break;
}
}
if(suc==1) //第cur行摆放成功,则继续搜索,摆放第(cur+1)行
dfs(cur+1);
}
}
int main()
{
scanf("%d",&n);
memset(map,0,sizeof(map));
dfs(0); //从第0行开始放置皇后
printf("%d\n",ans);
return 0;
}
法二:对上述方法中程序的“优化”
可以使用二维数组vis直接判断当前尝试的皇后所在的列和两个对角线是否已经有其它皇后。注意到主对角线标识y-x可能为负,因此在存取时要+n。
#include <stdio.h>
#include <string.h>
#define maxn 20
int n;
int ans=0;
int map[maxn];
int vis[3][maxn];
void dfs(int cur)
{
int i;
if(cur==n)
{
//输出n个皇后所在列的列号
/*for(i=0;i<n;i++)
printf("%d ",map[i]);
printf("\n"); */
ans++;
return;
}
for(i=0;i<n;i++)
{
//该列没有放过棋子 且 主对角线没有放过棋子 且 副对角线没有放过棋子
if(!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n])
{
vis[0][i]=1;
vis[1][cur+i]=1;
vis[2][cur-i+n]=1;
map[cur]=i+1;
dfs(cur+1);
vis[0][i]=0;
vis[1][cur+i]=0;
vis[2][cur-i+n]=0;
}
}
}
int main()
{
scanf("%d",&n);
memset(map,0,sizeof(map));
memset(vis,0,sizeof(vis));
dfs(0);
printf("%d\n",ans);
return 0;
}
以上两种方法总结:
与保存n*n棋盘上每个位置是否有皇后相比,这里使用一维数组map[maxn]保存每行中皇后所在的列号,降低了空间上的开销,是更好的方法。
如果题目只要求输出方案总数,上面的map数组可省略;
当然,如果程序要求输出摆放好皇后的n*n棋盘的话,就需要使用二维数组了。
(Ⅱ)N皇后问题变种+二维数组情境下的DFS——工作分配问题
【问题描述】
设有n项工作分配给n个人。将工作i分配给第j个人所需的费用为cij。试设计一个算法,为每一个人都分配一件不同的工作,并使总费用达到最小。
【编程任务】
设计一个算法,对于给定的工作费用,计算最佳工作分配方案,使总费用达到最小。
【输入格式】
由文件job.in给出输入数据。第一行有1个正整数n (1≤n≤16)。接下来的n行,每行n个数,第i行表示第i个人各项工作费用。
【输出格式】
将计算出的最小总费用输出到文件job.out。
【输入样例】
3
4 2 5
2 3 6
3 4 5
【输出样例】
9
【分析】N皇后问题变种+二维数组情境下的DFS
总体思路同上述(Ⅰ),只是这里没有了“对角线判定”。
这里可以将二维数组的行看做各个人,列看做各项工作(1<=i, j<=n)。此题的情景(n项工作分配给n个人)相当于在n*n二维数组中每行各选取一个数,使得每行每列只有1个数被选中。
需要注意的是:因为所求解为最优解,故在搜索过程中要注意“剪枝”。在此题中,若发现当前的费用总和超过了之前所求出的最优解,则停止搜索,以提高效率。
#include <stdio.h>
#include <math.h>
#include <string.h>
#define maxn 20
#define INF pow(2,31)-1
int n;
int money[maxn][maxn]; //第i个人分配到第j项工作可获得的费用
int vis[maxn]; //vis[j]记录第j项工作是否已被分配 1-是 0-否
int minsum=2147483647; //最小费用总和
void dfs(int i,int sum) //从第i个人开始分配工作,当前费用总和sum
{
int j;
int temp;
//剪枝:当前费用总和>最小费用总和,表示当前的分配方案不合题意,不再继续搜索
if(sum>minsum)
return;
//n个人的工作分配完成
if(i==n)
{
//当前费用总和>最小费用总和,则更新
if(sum<minsum)
minsum=sum;
return;
}
//遍历n项工作
for(j=0;j<n;j++)
{
//第j项工作可分配给第i个人
if(!vis[j])
{
//分配第j项工作
vis[j]=1;
//继续给第i+1个人分配工作,此时当前费用总和+=money[i][j]
dfs(i+1,sum+money[i][j]);
//回溯
vis[j]=0;
}
}
}
int main()
{
int i,j;
scanf("%d",&n);
for(i=0;i<n;i++)
for(j=0;j<n;j++)
scanf("%d",&money[i][j]);
memset(vis,0,sizeof(vis));
dfs(0,0); //从第1个人开始分配工作
printf("%d\n",minsum);
return 0;
}
4. “行走路径”相关问题
(Ⅰ)马的遍历
中国象棋半张棋盘如图(a)所示。马自左下角往右上角跳。今规定只许往右跳,不许往左跳。4个可能的跳马方向如图(b)所示。
比如图(a)中所示为一种跳行路线,并将所经路线打印出来。
打印格式为:0,0->2,1->3,3->1,4->3,5->2,7->4,8…
【分析】如图(b),马最多有四个方向,若原来的横坐标为j、纵坐标为i,则四个方向的移动可表示为:
1: (i, j)→(i-2, j+1); (i<3,j<8)
2: (i, j)→(i-1, j+2); (i<4,j<7)
3: (i, j)→(i+1, j+2); (i>0,j<7)
4: (i, j)→(i+2, j+1); (i>1,j<8)
故有搜索策略:
S1: 起点(0, 0);
S2: 从起点出发,按移动规则依次选定某个方向,如果达到的是(4,8)则转向S3,否则继续搜索下一个到达的顶点;
S3: 打印路径。
需要注意的是:马是在方格点上行走的,而不是在方格中,因此图中的4*8棋盘实质上是5*9大小的格点。
#include <stdio.h>
#include <string.h>
#define maxnode 50
int total=0; //total记录马的行走路径总数
int map[5][9]; //记录5*9地图上的45个点马是否走过
struct Path //保存马的行走路径
{
int x;
int y;
} p[maxnode];
int dir[4][2]={{-2,1},{-1,2},{1,2},{2,1}}; //马的4个行走方向
//x.y-马的当前位置 ans-当前行走路径中点的个数
void dfs(int x,int y,int ans)
{
int i;
int xx,yy; //马的新位置
//到达终点,停止搜索,输出路线
if(x==4 && y==8)
{
for(i=0;i<ans-1;i++)
printf("%d,%d->",p[i].x,p[i].y);
printf("%d,%d\n",p[ans-1].x,p[ans-1].y);
total++;
return;
}
for(i=0;i<4;i++)
{
//获得马的新位置
xx=x+dir[i][0];
yy=y+dir[i][1];
//如果新位置没有出界,并且马之前没有走过,则走到新位置
//并保存路径,继续搜索
if(xx>=0 && xx<5 && yy>=0 && yy<9 && !map[xx][yy])
{
map[xx][yy]=1;
p[ans].x=xx;
p[ans].y=yy;
ans++;
dfs(xx,yy,ans);
map[xx][yy]=0;
ans--;
}
}
}
int main()
{
memset(map,0,sizeof(map));
memset(p,0,sizeof(p));
map[0][0]=1; //起点(0,0),设置为已走过,并且加入行走路径中
p[0].x=0;
p[0].y=0;
dfs(0,0,1); //从起点开始搜索
printf("total=%d\n",total);
return 0;
}
(Ⅱ)跳马问题
在5*5格的棋盘上,有一只中国象棋的马,从(1,1)点出发,按日字跳马,它可以朝8个方向跳,但不允许出界或跳到已跳过的格子上,要求在跳遍整个棋盘。
输出前5个方案及总方案数。
输出格式示例:
1 16 21 10 25
20 11 24 15 22
17 2 19 6 9
12 7 4 23 14
3 18 13 8 5
【分析】总体思路同上述(Ⅰ)。注意输出前5种方案(下述搜索过程已经保证结果按字典序输出)及总方案数,而不是输出前5种方案后搜索就停止!
#include <stdio.h>
#include <string.h>
int ans=0;
int map[5][5];
int vis[5][5];
int dir[8][2]={{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
void dfs(int x,int y,int cur)
{
int i,j;
int xx,yy;
if(cur==26)
{
if(ans<5)
{
for(i=0;i<5;i++)
{
for(j=0;j<4;j++)
printf("%d ",map[i][j]);
printf("%d\n",map[i][4]);
}
printf("\n");
}
ans++;
return;
}
for(i=0;i<8;i++)
{
xx=x+dir[i][0];
yy=y+dir[i][1];
if(xx>=0 && xx<5 && yy>=0 && yy<5 && !vis[xx][yy])
{
vis[xx][yy]=1;
map[xx][yy]=cur;
cur++;
dfs(xx,yy,cur);
vis[xx][yy]=0;
cur--;
}
}
}
int main()
{
map[0][0]=1;
vis[0][0]=1;
dfs(0,0,2);
printf("total=%d\n",ans);
return 0;
}
5. 整数划分问题
(Ⅰ)任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
当n=7共14种拆分方法:
7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4
total=14
【分析】结合划分问题的思路,输出所有的具体方案及总方案数。需要注意的是,某种组合的所有排列应该算作一种情况,因此在输出时,为了“去重”,可以按照样例输出,将所有数按照不递减顺序输出。
#include <stdio.h>
#include <string.h>
#define maxn 22
int n;
int total=0;
int num[maxn];
void dfs(int cur,int sum)
{
int i;
if(sum>n)
return;
if(sum==n)
{
for(i=0;i<cur-1;i++)
printf("%d+",num[i]);
printf("%d\n",num[cur-1]);
total++;
return;
}
for(i=1;i<n;i++)
{
//当前数i要大于等于前面的数,且不超过n,符合条件时可以填入
if(i>=num[cur-1])
{
num[cur]=i;
dfs(cur+1,sum+i);
num[cur]=0;
}
}
}
int main()
{
scanf("%d",&n);
memset(num,0,sizeof(num));
dfs(0,0);
printf("total=%d\n",total);
return 0;
}
(Ⅱ)将整数n分成k份,且每份不能为空,任意两种分法不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种分法被认为是相同的。
1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。
输入格式:n,k (6<n≤200,2≤k≤6)
输出格式:一个整数,即不同的分法。
样例输入:
7 3
样例输出:
4
{ 4种分法为:1,1,5;1,2,4;1,3,3; 2,2,3 说明部分不必输出 }
【分析】总体思路同上述(Ⅰ)
#include <stdio.h>
#define maxn 10
int n,k;
int ans=0;
int ret[maxn];
void dfs(int sum,int cur)
{
int i;
if(cur==k)
{
if(sum==n)
{
for(int i=0;i<cur;i++)
printf("%d ",ret[i]);
printf("\n");
ans++;
}
return;
}
for(i=1;i<=n;i++)
{
if(cur==k-1 && sum+i!=n)
continue;
if((cur>0 && i>=ret[cur-1]) || (cur==0))
{
sum+=i;
ret[cur++]=i;
dfs(sum,cur);
sum-=i;
ret[cur--]=0;
}
}
}
int main()
{
scanf("%d %d",&n,&k);
if(n<k)
ans=0;
else if(n==1 || k==1)
ans=1;
else
dfs(0,0);
printf("%d\n",ans);
return 0;
}
(Ⅲ)子集和问题
【问题描述】
子集和问题的一个实例为〈S,t〉。其中,S={ x1, x2,…, xn}是一个正整数的集合,c是一个正整数。子集和问题判定是否存在S的一个子集S1,使得子集S1和等于c。
【编程任务】
对于给定的正整数的集合S={ x1, x2,…, xn}和正整数c,编程计算S 的一个子集S1,使得子集S1和等于c。
【输入格式】
由文件subsum.in提供输入数据。文件第1行有2个正整数n和c,n表示S的个数,c是子集和的目标值。接下来的1 行中,有n个正整数,表示集合S中的元素。
【输出格式】
程序运行结束时,将子集和问题的解输出到文件subsum.out中。当问题有解时,任意输出一个解即可;当问题无解时,输出“No solution!”。
【输入样例】
5 10
2 2 6 5 4
【输出样例】
2 2 6
【分析】“划分”问题变种。需要注意去重及搜索的结束条件。
#include <stdio.h>
#include <string.h>
#define maxn 22
int n,c; //n个数,目标子集和c
int S[maxn]; //保存输入的n个数
//ret数组记录符合条件的子集 vis记录S数组中的数是否被使用(1-是 0-否)
int ret[maxn],vis[maxn];
int suc=0; //suc记录是否找到第一组解,1-是 0-否
//搜索:cur-当前子集最大元素下标 sum-当前子集和
//cntindex-每次加入子集的元素下标必须>=cntindex,以保证搜索是从cntindex开始向前进行的
void dfs(int cur,int cntindex,int sum)
{
int i;
//找到第一组解了,直接返回,不再搜索
if(suc==1)
return;
//当前子集和sum>目标子集和c,该子集不合题意,直接返回,不再搜索
if(sum>c)
return;
//当前子集和sum=目标子集和c,说明该子集符合题意
if(sum==c)
{
//输出子集
for(i=0;i<cur-1;i++)
printf("%d ",ret[i]);
printf("%d\n",ret[cur-1]);
//置suc为1,表示已经找到第一组解
suc=1;
return;
}
//遍历
for(i=0;i<n;i++)
{
//S[i]未被使用并且i比cntindex大
if(!vis[i] && i>=cntindex)
{
//剪枝:sum加上S[i]比目标子集和c大时,跳过S[i]
//看后面的元素是否符合条件
if(sum+S[i]>c)
continue;
//sum+S[i]<=c,搜索
vis[i]=1;
ret[cur]=S[i];
dfs(cur+1,i,sum+S[i]);
vis[i]=0;
}
}
}
int main()
{
int i;
int total=0; //total记录S中元素之和
scanf("%d %d",&n,&c);
for(i=0;i<n;i++)
{
scanf("%d",&S[i]);
total+=S[i];
}
if(total<c) //预判断:total比c小时,可判断无解
printf("No solution!\n");
else //其它情况:搜索
{
memset(vis,0,sizeof(vis));
dfs(0,0,0);
if(suc==0) //如果未找到第一组符合条件的解,给出相应无解的输出
printf("No solution!\n");
}
return 0;
}
6. 线性搜索举例
(Ⅰ)字符序列
【问题描述】
从三个元素的集合[A,B,C]中选取元素生成一个N个字符组成的序列,使得没有两个相邻字的子序列(子序列长度=2)相同。例:N = 5时ABCBA是合格的,而序列ABCBC与ABABC是不合格的,因为其中子序列BC,AB是相同的。
对于由键盘输入的N(1<=N<=12),求出满足条件的N个字符的所有序列和其总数。
【输入样例】
4
【输出样例】
72
【分析】线性搜索,注意对子序列的判断
#include <stdio.h>
#include <string.h>
#define maxlen 15
int N;
int ans=0; //ans记录符合要求的字符序列数
char ret[maxlen]; //ret数组记录符合要求的字符序列 '0'代表空位
int Judge(char *s,int N) //子序列判断
{
int i;
//找是否有相邻且长度为2的子序列
for(i=0;i+3<N;i++)
{
if(s[i]==s[i+2] && s[i+1]==s[i+3])
return 0;
}
return 1;
}
void dfs(int cur) //填充字符序列的第cur个字符
{
int i;
char ch;
//搜索终点
if(cur==N)
{
//求得的字符序列符合要求,输出之,方案数ans+1
if(Judge(ret,cur))
{
for(i=0;i<cur;i++)
printf("%c",ret[i]);
printf("\n");
ans++;
}
return;
}
//填充字符序列
for(ch='A';ch<='C';ch++)
{
if(ret[cur]=='0')
{
ret[cur]=ch;
dfs(cur+1);
ret[cur]='0';
}
}
}
int main()
{
int i;
scanf("%d",&N);
memset(ret,'0',sizeof(ret));
dfs(0); //从第0个字符开始,填充字符序列
printf("%d\n",ans);
return 0;
}
(Ⅱ)试卷批分
【问题描述】
某学校进行了一次英语考试,共有10道是非题,每题为10分,解答用1表示“是”,用0表示“非”的方式。但老师批完卷后,发现漏批了一张试卷,而且标准答案也丢失了,手头只剩下了3张标有分数的试卷。
试卷一:① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩
0 0 1 0 1 0 0 1 0 0 得分:70
试卷二:① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩
0 1 1 1 0 1 0 1 1 1 得分:50
试卷三:① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩
0 1 1 1 0 0 0 1 0 1 得分:30
待批卷:① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩
0 0 1 1 1 0 0 1 1 1 得分:?
【问题求解】:
请编一程序依据这三张试卷,算出漏批的那张试卷的分数。
【分析】线性搜索
需要注意的是,搜索结束可发现共有4组解,不过每组解对应的待批卷分数均相同(可谓巧合!)
#include <stdio.h>
#include <string.h>
char a[]="0010100100"; //得分:70
char b[]="0111010111"; //得分:50
char c[]="0111000101"; //得分:30
char d[]="0011100111";
char ret[10];
void dfs(int cur)
{
char i;
int j;
int sc1=0,sc2=0,sc3=0,sc4=0;
if(cur==10)
{
for(j=0;j<10;j++)
{
if(a[j]==ret[j])
sc1+=10;
if(b[j]==ret[j])
sc2+=10;
if(c[j]==ret[j])
sc3+=10;
}
if(sc1==70 && sc2==50 && sc3==30)
{
for(j=0;j<10;j++)
printf("%c-",ret[j]);
printf("\n");
for(j=0;j<10;j++)
{
if(d[j]==ret[j])
sc4+=10;
}
printf("sc4=%d\n",sc4);
}
return;
}
for(i='0';i<='1';i++)
{
if(ret[cur]=='A')
{
ret[cur]=i;
dfs(cur+1);
ret[cur]='A';
}
}
}
int main()
{
memset(ret,'A',sizeof(ret));
dfs(0);
return 0;
}