回溯法
- 回溯法指从初始状态出发,按照深度优先搜索的方式,根据产生子节点的条件约束,搜索问题的解,当发现当前节点不满足求解条件时,就回溯,尝试其他路径。
- 回溯法是一种“能进则进,进不了就换,换不了则退”的搜索方法。
- 用回溯法解决实际问题时,首先要确定解的组织形式,定义问题的解空间。
解空间
- 解的组织形式:回溯法的解的组织形式可以被规范为一个 n n n元组 { x 1 , x 2 , . . . , x n } \{x_{1},x_{2},...,x_{n}\} {x1,x2,...,xn},例如对3个物品的0-1背包问题,解的组织形式为 { x 1 , x 2 , x 3 } \{x_{1},x_{2},x_{3}\} {x1,x2,x3}
- 显约束:对解分量的取值范围的限定。
- 解空间:由所有可能解组成的空间。解空间越小,搜索效率越高,解空间越大,搜索效率越低。
- 隐约束:指对能否得到问题的可行解或最优解做出的约束。
- 隐约束(剪枝函数)包括约束函数和限界函数。判断能否得到可行解的函数被称为约束函数,判断能否得到最优解的函数被称为限界函数。
- 有了剪枝函数,就可以剪掉得不到可行解或最优解的分支,避免无效搜索,提高搜索效率。剪枝函数设计得好,搜索效率就高。
- 在搜索解空间时,有以下几个术语需要说明。
- 扩展节点:一个正在生成孩子的节点。
- 活节点:一个自身已生产,但孩子还没有全部生成的节点。
- 死节点:一个所有孩子都已经生成的节点
- 子孙:节点e的子树上所有节点都是e的子孙
- 祖宗:从节点e到树根路径上的所有节点都是e的祖宗
解题秘籍
- 定义解空间,因为解空间的大小对搜索效率有很大的影响,因此使用回溯法时首先要定义合适的解空间,包括解的组织形式和显约束。
- 解的组织形式:将解的组织形式都规范为一个 n n n元组 { x 1 , x 2 , . . . , x n } \{x_{1},x_{2},...,x_{n}\} {x1,x2,...,xn},只是对具体问题表达的含义不同而已。
- 显约束:显约束是对解分量的取值范围的限定,可以控制解空间的大小。
- 确定解空间的组织结构。解空间的组织结构通常以解空间树形象地表达,根据解空间树的不同,解空间分为子集数、排列数、m叉树等。
- 搜索解空间,按照深度优先搜索策略,根据隐约束(约束函数和限界函数),在解空间中搜索问题的可行解或最优解。当发现当前节点不满足求解条件时,就回溯,尝试其他路径。如果问题只是求可行解,则只需设定约束函数即可,如果要求最优解,则需要设定约束函数和限界函数。解空间和隐约束是控制搜索效率的关键。显约束可以控制解空间的大小,约束函数决定剪枝的效率,限界函数决定是否得到最优解。所以回溯法解题的关键是设计有效的显约束和隐约束
子集树
- 假设现在有4个物品和1个背包,每个物品的重量w都为(2,5,4,3),价值v都为(6,3,5,4),背包的容量为10(W = 10)。求在不超过背包容量的前提下把哪些物品放入背包,才能获得最大价值
算法设计
- 计算上界。计算已装入物品的价值
cp
及剩余的物品价值的总价值rp
。我们已经知道已被装入背包的物品价值cp
,对剩余的物品不确定要装入哪些,按照假设都被装入的情况计算,即按最大值计算(剩余的物品的总价值),因此得到的值是可装入物品价值的上界。 - 按约束条件和限界条件搜索求解。
t
表示当前扩展节点在第t
层,cw
表示当前已被放入物品的总量,cp
表示当前已被放入物品的价值。如果t>n
,则表示已经到达叶子,记录最优值的最优解,返回;否则,判断是否满足约束条件,满足则搜索左子树。因为左子树表示放入该物品,所以令x[t] = 1
,表示放入第t
个该物品。cw+=w[t]
,表示当前已被放入物品的重量增加w[t]
。cp+=v[t]
,表示当前已被放入第t
个该物品。cw+=w[t]
,表示当前已被放入物品的价值增加v[t]
。Backtrack(t+1)
表示递推,深度优先搜索第t+1
层。回归时即向上回溯时,把增加的值减去,cw-=w[t]
,cp-=v[t]
。判断是否满足限界条件,满足则搜索右子树。因为右子树表示不放入该物品,所以令x[t] = 0
,当前已被放入物品的重量,价值均不改变。Backtrack(t+1)
表示深度优先搜索第t+1
层
算法实现
#include<iostream>
using namespace std;
const int M=105;
int i,j,n,W;//n表示n个物品,W表示背包的容量
double w[M],v[M];//w[i] 表示第i个物品的重量,v[i] 表示第i个物品的价值
bool x[M]; //x[i]表示第i个物品是否放入背包
double cw; //当前重量
double cp; //当前价值
double bestp; //当前最优价值
bool bestx[M]; //当前最优解
double Bound(int i); //计算上界(即已装入物品价值+剩余物品的总价值)
void Backtrack(int t); //用于搜索空间数,t表示当前扩展结点在第t层
void Knapsack(double W, int n);
int main(){
cout<<"请输入物品的个数n和背包的容量W:";
cin>>n>>W;
cout<<"请依次输入每个物品的重量w和价值v,用空格分开:"<<endl;
for(i=1;i<=n;i++)
cin>>w[i]>>v[i];
Knapsack(W,n);
return 0;
}
double Bound(int i){//计算上界(即已装入物品价值+剩余物品的总价值)
//剩余物品为第i~n种物品
int rp=0;
while(i<=n){//依次计算剩余物品的价值
rp+=v[i];
i++;
}
return cp+rp;
}
void Backtrack(int t){//用于搜索空间数,t表示当前扩展结点在第t层
if(t>n){//已经到达叶子结点
for(j=1;j<=n;j++)
bestx[j]=x[j];
bestp=cp;//保存当前最优解
return ;
}
if(cw+w[t]<=W){//如果满足约束条件则搜索左子树
x[t]=1;
cw+=w[t];
cp+=v[t];
Backtrack(t+1);
cw-=w[t];
cp-=v[t];
}
if(Bound(t+1)>bestp){//如果满足限界条件则搜索右子树
x[t]=0;
Backtrack(t+1);
}
}
void Knapsack(double W, int n){
cw=0;//初始化当前放入背包的物品重量为0
cp=0; //初始化当前放入背包的物品价值为0
bestp=0; //初始化当前最优值为0
double sumw=0.0; //用来统计所有物品的总重量
double sumv=0.0; //用来统计所有物品的总价值
for(i=1;i<=n;i++){
sumv+=v[i];
sumw+=w[i];
}
if(sumw<=W){
bestp=sumv;
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"所有的物品均放入背包。";
return;
}
Backtrack(1);
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"放入背包的物品序号为: ";
for(i=1;i<=n;i++){ //输出最优解
if(bestx[i]==1)
cout<<i<<" ";
}
cout<<endl;
}
输入:
4 10
2 6
5 3
4 5
2 4
输出:
放入背包的物品最大价值为: 15
放入背包的物品序号为: 1 3 4
算法优化
- 在上面的程序中,上界函数是当前价值
cp
加剩余物品的总价值rp
,这个估值过高,因为剩余物品的重量很有可能是超过背包容量的。可以缩小上界,加快剪枝速度,提高搜索效率。 - 将上界函数
Bound()
改为:当前价值cp+
剩余容量可容纳的剩余物品的最大价值brp
。 - 为了更好地计算和运用上界函数剪枝,先将物品按照其单位总量价值(价值/重量)从大到小排序,然后按照排序后的顺序考察各个物品。
#include<iostream>
#include<algorithm>//sort函数需要该头文件
using namespace std;
const int M=105;
int i,j,n,W;//n表示n个物品,W表示背包的容量
double w[M],v[M];//w[i] 表示第i个物品的重量,v[i] 表示第i个物品的价值
bool x[M]; //x[i]表示第i个物品是否放入背包
double cw; //当前重量
double cp; //当前价值
double bestp; //当前最优价值
bool bestx[M]; //当前最优解
struct Object{//定义物品结构体,包含物品序号和单位重量价值
int id; //物品序号
double d;//单位重量价值
};
bool cmp(Object a1,Object a2); //按照物品单位重量价值由大到小排序
double Bound(int i); //计算上界(即将剩余物品装满剩余的背包容量时所能获得的最大价值)
void Backtrack(int t); //用于搜索空间数,t表示当前扩展结点在第t层
void Knapsack(int W,int n);
int main(){
cout<<"请输入物品的个数n和背包的容量W:";
cin>>n>>W;
cout<<"请依次输入每个物品的重量w和价值v,用空格分开:"<<endl;
for(i=1;i<=n;i++)
cin>>w[i]>>v[i];
Knapsack(W,n);
return 0;
}
double Bound(int i){//计算上界(即将剩余物品装满剩余的背包容量时所能获得的最大价值)
//i表示剩余物品为第i~n种物品
double cleft=W-cw;//剩余容量
double brp=0.0;
while(i<=n&&w[i]<cleft){
cleft-=w[i];
brp+=v[i];
i++;
}
if(i<=n)//装满背包
brp+=v[i]/w[i]*cleft;
return cp+brp;
}
void Backtrack(int t){//用于搜索空间数,t表示当前扩展结点在第t层
if(t>n){//已经到达叶子结点
for(j=1;j<=n;j++)
bestx[j]=x[j];
bestp=cp;//保存当前最优解
return ;
}
if(cw+w[t]<=W){//如果满足限制条件则搜索左子树
x[t]=1;
cw+=w[t];
cp+=v[t];
Backtrack(t+1);
cw-=w[t];
cp-=v[t];
}
if(Bound(t+1)>bestp){//如果满足限制条件则搜索右子树
x[t]=0;
Backtrack(t+1);
}
}
bool cmp(Object a1,Object a2){//按照物品单位重量价值由大到小排序
return a1.d>a2.d;
}
void Knapsack(int W,int n){
double sumw=0; //用来统计所有物品的总重量
double sumv=0; //用来统计所有物品的总价值
Object Q[n]; //物品结构体类型,用于按单位重量价值(价值/重量比)排序
double a[n+1],b[n+1];//辅助数组,用于把排序后的重量和价值赋值给原来的重量价值数组
for(i=1;i<=n;i++){
Q[i-1].id=i;
Q[i-1].d=1.0*v[i]/w[i];
sumv+=v[i];
sumw+=w[i];
}
if(sumw<=W){
bestp=sumv;
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"所有的物品均放入背包。";
return;
}
sort(Q,Q+n,cmp);
for(i=1;i<=n;i++){
a[i]=w[Q[i-1].id];//把排序后的数据传递过去
b[i]=v[Q[i-1].id];
}
for(i=1;i<=n;i++){
w[i]=a[i];//把排序后的数据传递过去
v[i]=b[i];
//cout<<"排序后的重量和价值为: "<<w[i]<<" "<<v[i]<<endl;
}
Backtrack(1);
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"放入背包的物品序号为: ";
for(i=1;i<=n;i++){
if(bestx[i]==1)
cout<<Q[i-1].id<<" ";
}
cout<<endl;
}
输入:
4 10
2 6
5 3
4 5
2 4
输出:
放入背包的物品最大价值为: 15
放入背包的物品序号为: 1 3 4
m叉树
- 给定无向连通图G和m种颜色,找出所有不同的着色方案,使相邻的区域有不同的颜色。如果把地图上的每一个区域都退化为一个点,将相邻的区域用线连接起来,地图就变成了一个无向连通图,给地图着色相当于给该无向连通图的每个点都着色,要求有连线的点不能有相同的颜色。
- 每个节点都有m种选择,即在解空间树中每个节点都有m个分支,称之为m叉树
算法设计
- 约束函数。假设当前扩展节点处于解空间树的第
t
层,那么从第1个节点到第t-1
个节点的状态(着色的色号)已经确定。接下来沿着扩展节点的第1个分支进行扩展,此时需要判断第t
个节点的着色情况。第t
个节点的颜色号要与前t-1
个节点中与其有边相连的节点颜色不一样。如果有一个颜色相同,则第t
个节点不能用这个色号,换下一个色号尝试。
bool OK(int t){ //约束条件
for(int j = 1; j < t;j++){ //依次判断前t-1个节点(已确定色号)
if(map[t][j]){ //如果t与j邻接(有边相连)
if(x[j] == x[t]){ //判断t与j的色号是否相同
return false; //有相同色号,返回false
}
}
}
return true; //与前t-1个节点中与其有边相连的节点颜色均不同,返回true
}
- 按约束条件搜索求解。
t
表示当前扩展节点在第t
层。如果t>n
,则表示已经到达叶子sum
累计第几个着色方案,输出可行解。否则,扩展节点沿着第1个分支扩展,判断是否满足约束条件。如果满足,则进入深一层继续搜索;如果不满足,则扩展生成的节点被剪掉,换下一个色号尝试。如果所有色号都尝试完毕,则该节点变成死节点,向上回溯到离其最近的活节点,继续搜索。搜索到叶子时,找到一种着色方案。搜索到全部活节点都变成死节点为止。
void Backtrac(int t){ //搜索函数
if(t > n){
sum++;
cout<<"第"<<sum<<"种方案:";
for(int i = 1;i <= n;i++){ //输出该着色方案
cout << x[i] << " ";
}
cout << endl;
}
else{
for(int i = 1;i <= m;i++){ //对每个节点都尝试m种颜色
x[t] = i;
if(OK(t)){
Backtrack(t+1);
}
}
}
}
排列树
- 在 n × n n \times n n×n的棋盘上放置了彼此不受攻击的 n n n个皇后。按照国际象棋的规则,皇后可以攻击与之在同一行、同一列、同一斜线上的棋子。请在 n × n n \times n n×n的棋盘上放置 n n n个皇后,使其彼此不受攻击。
算法设计
- 约束函数。在第
t
行放置第t
个皇后时,第t
个皇后与前t-1
个已放置好的皇后不能同列或同斜线。如果有一个成立,则第t
个皇后不可以被放置在该位置。x[t] == x[j]
表示第t
个皇后与第j
个皇后同列,t-j == abs(x[t] - x[j])
表示第t
个皇后与第j
个皇后同斜线。 - 按约束条件搜索求解。
t
表示当前扩展节点在第t
层。如果t>n
,则表示已经到达叶子节点,记录最优值和最优解,返回。否则,分别判断n(i = 1…n)个分支,x[t] = i;判断每个分子是否满足约束条件,如果满足,则进入下一层Backtrack(t+1),否则考察下一个分支(兄弟节点)。
算法实现
#include<iostream>
#include<cmath> //求绝对值函数需要引入该头文件
#define M 105
using namespace std;
int n;//n表示n个皇后
int x[M]; //x[i]表示第i个皇后放置在第i行第x[i]列
long long countn; //countn表示n皇后问题可行解的个数
bool Place(int t); //判断第t个皇后能否放置在第i个位置
void Backtrack(int t);
int main()
{
cout<<"请输入皇后的个数 n:";
cin>>n;
countn=0;
Backtrack(1);
cout <<"答案的个数是:"<<countn<< endl;
return 0;
}
bool Place(int t) //判断第t个皇后能否放置在第i个位置
{
bool ok=true;
for(int j=1;j<t;j++) //判断该位置的皇后是否与前面t-1个已经放置的皇后冲突
{
if(x[t]==x[j]||t-j==fabs(x[t]-x[j]))//判断列、对角线是否冲突
{
ok=false;
break;
}
}
return ok;
}
void Backtrack(int t)
{
if(t>n) //如果当前位置为n,则表示已经找到了问题的一个解
{
countn++;
for(int i=1; i<=n;i++) //打印选择的路径
cout<<x[i]<<" ";
cout<<endl;
cout<<"----------"<<endl;
}
else
for(int i=1;i<=n;i++) //分别判断n个分支,特别注意i不要定义为全局变量,否则递归调用有问题
{
x[t]=i;
if(Place(t))
Backtrack(t+1); //如果不冲突的话进行下一行的搜索
}
}
输入:
5
输出:
1 3 5 2 4
----------
1 4 2 5 3
----------
2 4 1 3 5
----------
2 5 3 1 4
----------
3 1 4 2 5
----------
3 5 2 4 1
----------
4 1 3 5 2
----------
4 2 5 3 1
----------
5 2 4 1 3
----------
5 3 1 4 2
----------
答案的个数是:10
训练1:魅力手镯
题目描述
贝西在商场的珠宝店发现一个魅力手镯。她想从n(1 \leq n \leq 3402)个可用的装饰物种选择尽可能好的装饰物去装饰它。每个装饰物都有一个重量w_{i}(1 \leq w_{i} \leq 400),以及一个期望值d_{i}(1 \leq d_{i} \leq 100),最多可以使用一次。贝西希望装饰物的总重量不超过m(1 \leq m \leq 12880)。给定n和m,并列出装饰物的重量和期望值列表,计算可能的最大期望值之和。
输入:第1行包含两个整数n和m。接下来的n行,每行都包含两个整数,分别表示装饰物的重量和期望值。
输出:单行输出一个整数,它是在给定权重约束的情况下可以达到的最大期望值的总和
算法实现
#include<iostream>
#include<algorithm>//sort函数需要该头文件
using namespace std;
const int M=105;
int i,j,n,W;//n表示n个物品,W表示背包的容量
double w[M],v[M];//w[i] 表示第i个物品的重量,v[i] 表示第i个物品的价值
bool x[M]; //x[i]表示第i个物品是否放入背包
double cw; //当前重量
double cp; //当前价值
double bestp; //当前最优价值
bool bestx[M]; //当前最优解
struct Object{//定义物品结构体,包含物品序号和单位重量价值
int id; //物品序号
double d;//单位重量价值
};
double Bound(int i); //计算上界(即将剩余物品装满剩余的背包容量时所能获得的最大价值)
void Backtrack(int t); //用于搜索空间数,t表示当前扩展结点在第t层
bool cmp(Object a1,Object a2); //按照物品单位重量价值由大到小排序
void Knapsack(int W,int n);
int main(){
cin>>n>>W;
for(i=1;i<=n;i++)
cin>>w[i]>>v[i];
Knapsack(W,n);
return 0;
}
double Bound(int i){//计算上界(即将剩余物品装满剩余的背包容量时所能获得的最大价值)
//i表示剩余物品为第i~n种物品
double cleft=W-cw;//剩余容量
double brp=0.0;
while(i<=n&&w[i]<cleft){
cleft-=w[i];
brp+=v[i];
i++;
}
if(i<=n)//装满背包
brp+=v[i]/w[i]*cleft;
return cp+brp;
}
void Backtrack(int t){//用于搜索空间数,t表示当前扩展结点在第t层
if(t>n){//已经到达叶子结点
for(j=1;j<=n;j++)
bestx[j]=x[j];
bestp=cp;//保存当前最优解
return ;
}
if(cw+w[t]<=W){//如果满足限制条件则搜索左子树
x[t]=1;
cw+=w[t];
cp+=v[t];
Backtrack(t+1);
cw-=w[t];
cp-=v[t];
}
if(Bound(t+1)>bestp){//如果满足限制条件则搜索右子树
x[t]=0;
Backtrack(t+1);
}
}
bool cmp(Object a1,Object a2){//按照物品单位重量价值由大到小排序
return a1.d>a2.d;
}
void Knapsack(int W,int n){
double sumw=0; //用来统计所有物品的总重量
double sumv=0; //用来统计所有物品的总价值
Object Q[n]; //物品结构体类型,用于按单位重量价值(价值/重量比)排序
double a[n+1],b[n+1];//辅助数组,用于把排序后的重量和价值赋值给原来的重量价值数组
for(i=1;i<=n;i++){
Q[i-1].id=i;
Q[i-1].d=1.0*v[i]/w[i];
sumv+=v[i];
sumw+=w[i];
}
if(sumw<=W){
bestp=sumv;
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"所有的物品均放入背包。";
return;
}
sort(Q,Q+n,cmp);
for(i=1;i<=n;i++){
a[i]=w[Q[i-1].id];//把排序后的数据传递过去
b[i]=v[Q[i-1].id];
}
for(i=1;i<=n;i++){
w[i]=a[i];//把排序后的数据传递过去
v[i]=b[i];
//cout<<"排序后的重量和价值为: "<<w[i]<<" "<<v[i]<<endl;
}
Backtrack(1);
cout<<bestp<<endl;
}
输入:
4 6
1 4
2 6
3 12
2 7
输出:
23
训练2:图的m着色问题
题目描述
给定无向连通图G和m种不同的颜色。用这些颜色为图G的各节点着色,对每个节点都着一种颜色。如果有一种着色方案可以使图G中每条边的两个节点着不同的颜色,则称这个图是m可着色的。计算图的不同的着色方案数。
输入:第1行包含3个正整数 n n n、 k k k和 m m m,表示有n个节点、k条边和m种颜色。节点编号为1~n。在接下来的k行中,每行都有两个正整数u、v,表示在u、v之间有一条边。N \leq 100,k \leq 2500,保证答案不超过20000。
输出:单行输出不同的着色方案数。
算法实现
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=105;
int n,m,k,ans;
int g[maxn][maxn],x[maxn];
bool check(int t); //约束条件
void Backtrack(int t); //搜索函数
int main(){
scanf("%d%d%d",&n,&k,&m);
int u,v;
for(int i=1;i<=k;i++){
scanf("%d%d",&u,&v);
g[u][v]=g[v][u]=1;
}
Backtrack(1);
printf("%d",ans);
return 0;
}
bool check(int t){
for(int j=1;j<t;j++){
if(g[t][j]&&(x[t]==x[j])) //判断t与j色号是否相同
return 0;
}
return 1;
}
void Backtrack(int t){
if(t>n){ //到达叶子,找到一个着色方案
ans++;
return;
}
for(int i=1;i<=m;i++){ //对每个节点都尝试m种颜色
x[t]=i;
if(check(t))
Backtrack(t+1);
}
}
输入:
5 8 4
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5
输出:
48
训练3:N皇后问题
题目描述
在 N × N N \times N N×N的方格棋盘上放置N个皇后,使得它们不相互攻击(即任意两个皇后都不允许同行、同列,也不允许再与棋盘边框成45 ∘ ^{\circ} ∘角的斜线上。求有多少种合法的放置方案。
输入:输入包含多个测试用例,每个测试用例都包含一个正整数 N N N( N ≤ 10 N \leq 10 N≤10),表示棋盘和皇后的数量,如果 N = 0 N = 0 N=0,则表示结束。
输出:对每个测试用例,单行输出一个正整数,表示有多少种合法的放置方案
算法实现
#include<iostream>
#include<cmath> //求绝对值函数需要引入该头文件
#define M 105
using namespace std;
int n;//n表示n个皇后
int x[M]; //x[i]表示第i个皇后放置在第i行第x[i]列
long long countn; //countn表示n皇后问题可行解的个数
bool Place(int t); //判断第t个皇后能否放置在第i个位置
void Backtrack(int t);
int main()
{
while (cin >> n && n){
countn=0;
Backtrack(1);
cout <<countn<< endl;
}
return 0;
}
bool Place(int t) //判断第t个皇后能否放置在第i个位置
{
bool ok=true;
for(int j=1;j<t;j++) //判断该位置的皇后是否与前面t-1个已经放置的皇后冲突
{
if(x[t]==x[j]||t-j==fabs(x[t]-x[j]))//判断列、对角线是否冲突
{
ok=false;
break;
}
}
return ok;
}
void Backtrack(int t)
{
if(t>n) //如果当前位置为n,则表示已经找到了问题的一个解
{
countn++;
}
else
for(int i=1;i<=n;i++) //分别判断n个分支,特别注意i不要定义为全局变量,否则递归调用有问题
{
x[t]=i;
if(Place(t))
Backtrack(t+1); //如果不冲突的话进行下一行的搜索
}
}
输入:
1
8
5
0
输出:
1
92
10
DFS+剪枝优化
训练1:数独游戏
题目描述
数独是一项非常简单的任务。编写一个程序来解决给定的数独任务。
输入:输入数据将从测试用例的数量开始。对于每个测试用例,后门都跟9行,对应表的行,在每一行上都给出9个十进制数字,对应这一行中的单元格,如果单元格为空,则用0表示。
输出:对于每个测试用例,程序都应该以与输入数据相同的格式打印解决方案。空单元格必须按照规则填充。如果解决方案不是唯一的,那么程序可以打印其中任何一个。
算法设计
- 预处理输入数据
- 从左上角(1,1)开始按行搜索,如果行
i = 10
,则说明找到答案,返回1. - 如果
map[i][j]
已填数组,则判断如果列j = 9
,则说明处理到当前行的最后一列,继续下一行第1列的搜索,即dfs(i+1,1)
,否则在当前行的下一列搜索,即dfs(i,j+1)
。如果搜索成功,则返回1,否则返回0。 - 如果
map[i][j]
未填数字,则计算当前位置(i,j)
所属子网格。枚举数字1~9填空,如果当前行、当前列、当前子网络均未填该数字,则填写该数字并标记该数字已出现。如果判断列j = 9
,则说明处理到当前行的最后一列,继续下一行第1列的搜索,即dfs(i+1,1)
,否则在当前行的下一列搜索,即dfs(i,j+1)
。如果搜索失败,则回溯归位,继续搜索,否则返回1。
算法实现
#include<iostream>
#include<cstring>
using namespace std;
int map[10][10];
bool row[10][10];//row[i][x]标记在第i行中数字x是否已出现
bool col[10][10];//col[j][y]标记在第j列中数字y是否已出现
bool grid[10][10];//grid[k][z]标记在第k个3*3子格中数字z是否已出现
bool dfs(int i,int j); //深度搜索函数
void init(); //初始化函数
int main(){
int T;
cin>>T;
while(T--){
init();
dfs(1,1);
for(int i=1;i<=9;i++){
for(int j=1;j<=9;j++)
cout<<map[i][j];
cout<<endl;
}
}
return 0;
}
bool dfs(int i,int j){
if(i==10)
return 1;
bool flag=0;
if(map[i][j]){
if(j==9)
flag=dfs(i+1,1);
else
flag=dfs(i,j+1);
return flag?1:0;
}
else{
int k=3*((i-1)/3)+(j-1)/3+1;
for(int x=1;x<=9;x++){//枚举数字1~9填空
if(!row[i][x]&&!col[j][x]&&!grid[k][x]){
map[i][j]=x;
row[i][x]=1;
col[j][x]=1;
grid[k][x]=1;
if(j==9)
flag=dfs(i+1,1);
else
flag=dfs(i,j+1);
if(!flag){ //回溯,继续枚举
map[i][j]=0;
row[i][x]=0;
col[j][x]=0;
grid[k][x]=0;
}
else
return 1;
}
}
}
return 0;
}
void init(){
memset(row,false,sizeof(row));
memset(col,false,sizeof(col));
memset(grid,false,sizeof(grid));
char ch;
for(int i=1;i<=9;i++)
for(int j=1;j<=9;j++){
cin>>ch;
map[i][j]=ch-'0';
if(map[i][j]){
int k=3*((i-1)/3)+(j-1)/3+1;
row[i][map[i][j]]=1;
col[j][map[i][j]]=1;
grid[k][map[i][j]]=1;
}
}
}
输入:
1
103000509
002109400
000704000
300502006
060000050
700803004
000401000
009205800
804000107
输出:
143628579
572139468
986754231
391542786
468917352
725863914
237481695
619275843
854396127