算法训练营 搜索技术(深度优先搜索)

回溯法

  • 回溯法指从初始状态出发,按照深度优先搜索的方式,根据产生子节点的条件约束,搜索问题的解,当发现当前节点不满足求解条件时,就回溯,尝试其他路径。
  • 回溯法是一种“能进则进,进不了就换,换不了则退”的搜索方法。
  • 用回溯法解决实际问题时,首先要确定解的组织形式,定义问题的解空间

解空间

  • 解的组织形式:回溯法的解的组织形式可以被规范为一个 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的祖宗

解题秘籍

  1. 定义解空间,因为解空间的大小对搜索效率有很大的影响,因此使用回溯法时首先要定义合适的解空间,包括解的组织形式显约束
  • 解的组织形式:将解的组织形式都规范为一个 n n n元组 { x 1 , x 2 , . . . , x n } \{x_{1},x_{2},...,x_{n}\} {x1,x2,...,xn},只是对具体问题表达的含义不同而已。
  • 显约束:显约束是对解分量的取值范围的限定,可以控制解空间的大小。
  1. 确定解空间的组织结构。解空间的组织结构通常以解空间树形象地表达,根据解空间树的不同,解空间分为子集数、排列数、m叉树等。
  2. 搜索解空间,按照深度优先搜索策略,根据隐约束(约束函数和限界函数),在解空间中搜索问题的可行解或最优解。当发现当前节点不满足求解条件时,就回溯,尝试其他路径。如果问题只是求可行解,则只需设定约束函数即可,如果要求最优解,则需要设定约束函数限界函数解空间隐约束是控制搜索效率的关键。显约束可以控制解空间的大小约束函数决定剪枝的效率限界函数决定是否得到最优解。所以回溯法解题的关键是设计有效的显约束隐约束

子集树

  • 假设现在有4个物品和1个背包,每个物品的重量w都为(2,5,4,3),价值v都为(6,3,5,4),背包的容量为10(W = 10)。求在不超过背包容量的前提下把哪些物品放入背包,才能获得最大价值

算法设计

  1. 计算上界。计算已装入物品的价值cp及剩余的物品价值的总价值rp。我们已经知道已被装入背包的物品价值cp,对剩余的物品不确定要装入哪些,按照假设都被装入的情况计算,即按最大值计算(剩余的物品的总价值),因此得到的值是可装入物品价值的上界。
  2. 按约束条件和限界条件搜索求解。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叉树

算法设计

  1. 约束函数。假设当前扩展节点处于解空间树的第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
}
  1. 按约束条件搜索求解。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 N10),表示棋盘和皇后的数量,如果 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. 预处理输入数据
  2. 从左上角(1,1)开始按行搜索,如果行i = 10,则说明找到答案,返回1.
  3. 如果map[i][j]已填数组,则判断如果列j = 9,则说明处理到当前行的最后一列,继续下一行第1列的搜索,即dfs(i+1,1),否则在当前行的下一列搜索,即dfs(i,j+1)。如果搜索成功,则返回1,否则返回0。
  4. 如果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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽星_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值