算法笔记【二】DFS

DFS,就是暴力搜索,去枚举所有的方案,每个DFS都会对应于一个搜索树,我们再考虑DFS问题的时候,首先要考虑的就是顺序,用一个怎么样的顺序能得到所有的方案

快速入门

排列数字

题目描述
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。现在,请你按照字典序将所有的排列方法输出。

输入格式
共一行,包含一个整数 n。

输出格式
按字典序输出所有排列方案,每个方案占一行。

数据范围

1 <= n <= 7

题解

之前说过, 每个DFS都会对应一个搜索树, 我们要想一个顺序能够枚举到所有的方案, 从我们所画的这个树来看, 实际上就是暴力枚举每个位置上能放的数字。 每到一层我都会确定一个位置上的数, 当所有位置都被放上数字的时候, 就意味着这条分支已经末尾了,这个时候我就要开始准备回溯, 也就是从1 2 3的状态恢复到1 2 空, 可以理解为你进来的时候是什么样子, 你出去的时候就是什么样子。

这里举一个实际的例子,当周末你爸妈出去,留你一个人在家,走之前家里是干干净净的,如果说回来家里乱乱糟糟的,结果可想而知。为了不要挨打,你要在爸妈回来之前,把屋子收拾好,他们走之前是什么样子,回来就是什么样子的,这个过程实际上就是回溯的过程,下面让我们来看一看用代码如何实现。

代码实现

#include<iostream>
using namespace std;
const int N  = 10;
bool  st[N];//全排列中是不允许数字重复的,所以用一个bool数组来记录每个数是否被用过
int path[N];//用来记录结果
int n;
void dfs(int u){
    //如果说我们到达最后一层,说明我们找到了一组方案
    if(u == n){
        for(int i = 0;i<n;i++){
            cout<<path[i]<<" ";            
        }
        cout<<endl;
    }
    //向该位置上添加数字
    for(int i = 1;i<=n;i++){
        if(!st[i]){
        //如果说这个数在这条路径上没有用过,说明这个位置上可以用这个树来进行填充
            st[i] = true;
            path[u] = i;
            dfs(u+1);
            //恢复现场,我进入的时候是怎么样的,出去的时候就要是怎么样
            path[u] = 0;
            st[i] = false;
        }
    }
}
int main(){
    cin>>n;
    dfs(0);
    return 0 ;
}

N 皇后

题目描述
n− 皇后问题是指将 n 个皇后放在n × n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式
共一行,包含整数 n。

输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。

其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。

每个方案输出完成后,输出一个空行。

注意:行末不能有多余空格。

输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BahXtRJZ-1641720141168)(https://g.yuque.com/gr/latex?1%E2%89%A4n%E2%89%A49#card=math&code=1%E2%89%A4n%E2%89%A49&id=51fa6594)]

思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E9V7bAGR-1641720141169)(https://cdn.jsdelivr.net/gh/freedom0123/img/N%E7%9A%87%E5%90%8E.png#id=JpXI9&originHeight=981&originWidth=1892&originalType=binary&ratio=1&status=done&style=none)]
这个DFS所对应的树就是这样的, 从行开始枚举, 如左图, 如果说这个皇后放在这个位置上, 然后考虑下一行, 看一下, 在上一行的基础基础之上, 当前这一行中那些位置能够放皇后。 这里就会涉及到一个问题, 如何判断当前位置能不能放皇后呢, 根据题意,列,行, 正对角线, 反对角线上只能有一个皇后, 但是因为我们是按行开始枚举的, 所以说行中就不能有重复的皇后, 直接pass。 现在只剩下了列, 正对角线, 反对角线, 通过数学推导, 我们可以很容易得到两个对角线的方程, 通过方程我们知道, 这一条线上的所有点的横纵坐标加起来都对应与一个值, 我们可以通过这个映射值来判断当前这条线上是否有元素。 但是由于i-u 有可能会越界, 所以加一个偏移量, 可以这样理解, 这条线上所有点的横纵坐标都是相同的, 当他们同时加上一个n, 他们仍然是相同的。

代码描述

#include<iostream>
#include<cstring>
using namespace std;
const int N = 10;
char path[N][N];
bool col[N],dg[N*2],udg[N*2];
int n;
void dfs(int u){
    if(u == n){
        for(int i = 0 ;i < n; i++){
            cout<<path[i]<<endl;
        }
        cout<<endl;
        return;
    }
    for(int i = 0 ; i < n; i++){
        if(!dg[i+u] && !udg[i-u+n] && !col[i]){
            col[i] = dg[i+u] = udg[i-u+n] = true;
            path[u][i] = 'Q';
            dfs(u+1);
            col[i] = dg[i+u] = udg[i-u+n] = false;
            path[u][i] = '.';
        }
    }
}
int main(){
    cin>>n;
    for(int i = 0 ;i < n; i++){
        for(int j = 0;j < n; j++){
            path[i][j] = '.';
        }
    }
    dfs(0);
    return 0;
}

连通块模型

迷宫

题目描述
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由n∗n 的格点组成,每个格点只有2种状态,.#,前者表示可以通行后者表示不能通行。同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。如果起点或者终点有一个不能通行(为#),则看成无法办到。
注意:A、B不一定是两个不同的点。

输入格式
第1行是测试数据的组数 k,后面跟着 k 组输入。

每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n∗n 的。

接下来是一个 n∗n 的矩阵,矩阵中的元素为.或者#。

再接下来一行是 4 个整数 ha,la,hb,lb,描述 A 处在第 ha 行, 第 la 列,B 处在第 hb 行, 第 lb 列。

注意到 ha,la,hb,lb 全部是从 0 开始计数的。

输出格式
k行,每行输出对应一个输入。

能办到则输出“YES”,否则输出“NO”。

数据范围

1≤n≤100

思路
![BEED9F43-C632-4E18-BEC8-3DFE5D6CE611.jpeg](https://img-blog.csdnimg.cn/img_convert/8e9843b78d402f4b9609de70c5e2c1e0.png#clientId=u546c988a-3854-4&from=ui&id=ucd16d9d5&margin=[object Object]&name=BEED9F43-C632-4E18-BEC8-3DFE5D6CE611.jpeg&originHeight=626&originWidth=1537&originalType=binary&ratio=1&size=313429&status=done&style=none&taskId=u8a82f494-50c0-4812-9f14-9b975ca8f60)

代码描述

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N  = 110;
char path[N][N];
bool st[N][N];
int k;
int n;
int xa,ya,xb,yb;
int dx[4] = {-1,1,0,0};
int dy[4] = {0,0,-1,1};
bool dfs(int x,int y){
    
    if (path[x][y] == '#') return false;
    
    if (x == xb && y == yb) return true;
    
    st[x][y] = true;
    
    for (int i = 0; i < 4; i ++ ){
        
        int a = x + dx[i], b = y + dy[i];
        
        if (a < 0 || a >= n || b < 0 || b >= n) continue;
        if (st[a][b]) continue;
        if (dfs(a, b)) return true;
    }
    return false;
}
int main(){
    cin>>k;
    while(k--){
        cin>>n;
        for(int i = 0;i<n;i++){
            scanf("%s",path[i]);
        }
        scanf("%d%d%d%d",&xa,&ya,&xb,&yb);
        memset(st, 0, sizeof st);
        if(dfs(xa,ya)){
            cout<<"YES"<<endl;
        }else{
            cout<<"NO"<<endl;
        }
    }
    return 0;
}

红与黑

题目描述

有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。

你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。

请写一个程序,计算你总共能够到达多少块黑色的瓷砖。

输入格式

输入包括多个数据集合。

每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。

在接下来的 H 行中,每行包括 W 个字符。每个字符表示一块瓷砖的颜色,规则如下

1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。

当在一行中读入的是两个零时,表示输入结束。

输出格式

对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。

思路详解

每个dfs都会对应一棵树,而树都会有一个根节点,相当于这个棋盘中的起始位置,而这个节点都会有四个分支,相当于上下左右方向所到达的点,所以和上一题的树是一样的。本质上就是求这个连通块的大小

代码

#include<cstring>
#include<iostream>
using namespace std;
const int N  = 30;
bool st[N][N];
char path[N][N];
int n ,m;
int dx[4] = {-1,1,0,0};
int dy[4] = {0,0,1,-1};
int dfs(int x,int y){
    int cnt  =1;
    st[x][y] = true;
    for(int i = 0;i<4;i++){
        int a = x+dx[i];
        int b = y+dy[i];
        if(a<0 || a>=n || b<0 ||b>=m)  continue;
        if(st[a][b]) continue;
        if(path[a][b]!='.') continue;
        cnt+=dfs(a,b);
    }
    return cnt;
}
int main(){
    while(cin>>m>>n , n||m){
        for(int i = 0;i<n;i++){
            cin>>path[i];
        }
        int x ,y;
        for(int i = 0;i<n;i++){
            for(int j = 0;j<m;j++){
                if(path[i][j]=='@'){
                    x = i;
                    y = j;
                }
            }
        }
        memset(st,0,sizeof st);
        cout<<dfs(x,y)<<endl;
    }
    return 0;
}

搜索顺序

马走日

题目描述
马在中国象棋以日字形规则移动。
请编写一段程序,给定n∗m大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。

输入格式
第一行为整数 T,表示测试数据组数。

每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。

输出格式
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。

数据范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pKb7ndLE-1641720141172)(https://g.yuque.com/gr/latex?1%E2%89%A4T%E2%89%A49%2C%0A%0A1%E2%89%A4m%2Cn%E2%89%A49%2C%0A%0A0%E2%89%A4x%E2%89%A4n%E2%88%921%2C%0A%0A0%E2%89%A4y%E2%89%A4m%E2%88%921#card=math&code=1%E2%89%A4T%E2%89%A49%2C%0A%0A1%E2%89%A4m%2Cn%E2%89%A49%2C%0A%0A0%E2%89%A4x%E2%89%A4n%E2%88%921%2C%0A%0A0%E2%89%A4y%E2%89%A4m%E2%88%921&id=4eb1106c)]

输入样例

1
5 4 0 0

输出样例

32

#include<iostream>
#include<cstring>
using namespace std;
const int N = 10;
int n,m,x,y,ans;
bool st[N][N];

int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};

void dfs(int x,int y,int cnt){
    
    if(cnt == n*m){
        ans++;
        return;
    }
    st[x][y] = true;
    
    for(int i = 0;i < 8;i++){
        int a = dx[i]+x;
        int b = dy[i]+y;
        if(a < 0 || a>=n || b < 0 || b>=m) continue;
        if(st[a][b]) continue;
        dfs(a,b,cnt+1);    
    }
    st[x][y] = false;
}
int main(){
    int t;
    cin>>t;
    while(t--){
        memset(st,0,sizeof st);
        ans  = 0;
        cin>>n>>m>>x>>y;
        dfs(x,y,1);
        cout<<ans<<endl;
    }
    return 0;
}

单词接龙

分成互质组

题意描述
给定 n 个正整数,将它们分组,使得每组中任意两个数互质。

至少要分成多少个组?

输入格式

第一行是一个正整数 n。

第二行是 n 个不大于10000的正整数。

输出格式

一个正整数,即最少需要的组数。

数据范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xdEFdAm5-1641720141173)(https://g.yuque.com/gr/latex?1%E2%89%A4n%E2%89%A410#card=math&code=1%E2%89%A4n%E2%89%A410&id=126e38ae)]

输入样例:
6
14 20 33 117 143 175

输出样例:
3
思路

代码说明

#include <iostream>
#include <vector>

using namespace std;

const int N = 10;

int n;
int a[N]; // 存数字
int ans=N, len; // ans:全局答案 len:当前开的组数 
vector<int> g[N]; // 每一组

int gcd(int x,int y)
{
    return y ? gcd(y, x % y) : x;
}

bool check(int u,int c) // u:当前组,c:组编号,判断当前数能否放到这一组中
{
    for(int i=0;i<g[c].size();i++)
        if(gcd(g[c][i], u) > 1) return false;

    return true;
}

void dfs(int u)
{
    if(u==n){ // 处理完所有数字了
        ans = min(ans,len);
        return ;
    }

    // 不开新组
    for(int i = 0;i < len;i++){
        if(check(a[u], i)){
            g[i].push_back(a[u]);
            dfs(u+1);
            g[i].pop_back(); // 恢复现场
        }
    }
    // 开新组
    g[len++].push_back(a[u]);
    dfs(u + 1);
    g[--len].pop_back();
}

int main()
{
    cin>>n;
    for(int i=0;i<n;i++) cin>>a[i];

    dfs(0); // 从下标为0的数字开始搜

    cout<<ans<<endl;

    return 0;
}

剪枝

常用技巧

  1. 优先搜索分支较少的分支
  2. 不重复搜素:不会出现重复的结果
  3. 可行性剪枝:如果说当前方案不可行,就没有必要向下走
  4. 最优性剪枝:如果说当前分支已经比不上最优解了,就没有必要向下走了

小猫爬山

题目描述

翰翰和达达饲养了 N 只小猫,这天,小猫们要去爬山。

经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。

翰翰和达达只好花钱让它们坐索道下山。

索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是 C1、C2……CN。

当然,每辆缆车上的小猫的重量之和不能超过 W。

每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?

输入格式

第 1 行:包含两个用空格隔开的整数,N 和 W。

第 2…N+1 行:每行一个整数,其中第 i+1 行的整数表示第 i 只小猫的重量 Ci。

输出格式

输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。

数据范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AQ12pN2M-1641720141174)(https://g.yuque.com/gr/latex?1%E2%89%A4N%E2%89%A418%2C%0A1%E2%89%A4Ci%E2%89%A4W%E2%89%A4108#card=math&code=1%E2%89%A4N%E2%89%A418%2C%0A1%E2%89%A4Ci%E2%89%A4W%E2%89%A4108&id=b72cc05b)]

代码

import java.io.*;
import java.util.*;
import java.util.Collections;

public class Main {
    static int N  = 20;
    static int n,m;
    static int[] w = new int[N];
    static int[] s = new int[N];
    static String[] res = new String[2*N];
    static int ans = N;
    public static void reverse(){
        int i = 0;
        int j = n-1;
        while(i <= j){
            int t = w[i];
            w[i] = w[j];
            w[j] = t;
            i++;
            j--;
        }
    }

    public static void dfs(int u,int k){
        if(k >= ans){
            return;
        }
        if(u == n){
            ans = k;
            return;
        }
        for(int i = 0;i < k;i++){
            if(s[i] + w[u] <= m){
                s[i] += w[u];
                dfs(u+1,k);
                s[i] -= w[u];
            }
        }
        
        s[k] = w[u];
        dfs(u+1,k+1);
        s[k] = 0;
    }
    public static void main(String[] args) throws IOException {
        
        BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
        
        res = cin.readLine().split(" ");
        n = Integer.parseInt(res[0]);
        m = Integer.parseInt(res[1]);
    
        for(int i = 0; i < n ;i++){
            w[i] = Integer.parseInt(cin.readLine());
        }
        Arrays.sort(w,0,n);
        reverse();

        dfs(0,0);
        System.out.println(ans);
        cin.close();
    }
}

数独

题意描述
数独是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得图中每行、每列、每个 3×3 的九宫格内数字 1∼9 均恰好出现一次。

请编写一个程序填写数独。

输入格式
输入包含多组测试用例。

每个测试用例占一行,包含 81 个字符,代表数独的 81 个格内数据(顺序总体由上到下,同行由左到右)。

每个字符都是一个数字(1−9)或一个 .(表示尚未填充)。

您可以假设输入中的每个谜题都只有一个解决方案。

文件结尾处为包含单词 end 的单行,表示输入结束。

输出格式
每个测试用例,输出一行数据,代表填充完全后的数独。

思路
对于DFS来说, 最重要的是搜索的顺序, 要能够枚举到所有的方案, 这个题对应的一个DFS搜索顺序就是这样的,

  1. 每次任意选可以填的位置
  2. 枚举这个位置的所有可选方案
  3. 递归下一层

确定完搜索顺序之后, 就该考虑了, 在搜索的时候, 这个位置上能放哪个元素,根据数组本身的特点,这个位置上能够放的元素一定是这一行,这一列,这一个九宫格子中没有 出现过的。如果说集合来表示这种关系的话, 实际上就是这三个集合的一个交集。

这里我们使用一个二进制数来表示这一行中哪个元素没有被用过,如果这个二进制数的第 i 位是 1 ,就表示当前这一行 / 列 / 九宫格上,i 没有被用过,有 n 个 1 就表示这个位置上有几种选择( 这个操作可以使用 lowbit很容易算出来)。

下面我们来举一个例子 0 1 0 1 0 0 1 0 1 就表示当前 这行 / 列 / 九宫格 上,**1 3 6 8** 可以使用, 这里要注意的是, 二进制位是0 ~ 8 ,我们要映射为 1 ~ 9, 求交集就是将这三个数进行 & 运算即可

代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 9;
int row[N],col[N],cell[3][3];
int one[1<<N];//每个数的二进制表示中有多少个1
int map[1<<N];//快速求出 2 的幂
char str[100]; 
inline int lowbit(int x){
	return x&-x;
}
void init(){
	for(int i = 0;i < N;i++) row[i] = col[i] = (1 << N) - 1;	
	for(int i = 0; i < 3;i++){
		for(int j = 0;j < 3;j++){
			cell[i][j] = (1<<N)-1;
		}		
	}	
}
inline int get(int x,int y){
	return row[x] & col[y] & cell[x/3][y/3];
}
bool dfs(int cnt){
	if(!cnt) return true;
	// 寻找可用方案最小的横纵坐标 
	int minv = 10; 
	int x,y;//可选方案最小的横纵坐标
	for(int i = 0;i < N ;i++){
		for(int j = 0; j < N;j++){
			if(str[i*9 + j] == '.'){
				int t = one[get(i,j)];
				if(t < minv){
					x = i;
					y = j;
					minv = t;
				}
				
			}
		}
	} 
	
	for(int i = get(x,y);i != 0; i -= lowbit(i)){
		int t = map[lowbit(i)];
		row[x] -= 1<<t;
		col[y] -= 1<<t;
		cell[x / 3][y /3] -= 1<<t;
		str[x * 9 +y] = '1' + t; 
		if(dfs(cnt-1)) return true;
		
		row[x] += 1<<t;
		col[y] += 1<<t;
		cell[x / 3][y /3] += 1<<t;
		str[x * 9 +y] = '.'; 
		
	}	
	 
	return false;
}
int main(){
	for(int i = 0 ;i < N ;i++) map[1<<i] = i;
	for(int i = 0; i< 1<<N;i++) {
		int s  = 0;
		for (int j = i; j; j -= lowbit(j)) {
            s++;
        }
		one[i] = s;//i 的二进制表示中一共有多少个1 
	} 
	while(cin>>str,str[0] != 'e') {
		init();
		int cnt = 0;
		for(int i = 0 , k = 0; i < N;i++){
			for(int j = 0; j < N ;j++,k++){
				if(str[k]!='.'){
					int t  = str[k]-'1';
					row[i] -= 1<<t;
					col[j] -= 1<<t;
					cell[i/3][j/3] -= 1<<t;
				}else{
					cnt++;
				}
			}
			
		}
		dfs(cnt); 
		cout<<str<<endl;
	}
	return  0;
}

木棍

题意描述

乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。

然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。

请你设计一个程序,帮助乔治计算木棒的可能最小长度。

每一节木棍的长度都用大于零的整数表示。

输入格式

输入包含多组数据,每组数据包括两行。

第一行是一个不超过 64 的整数,表示砍断之后共有多少节木棍。

第二行是截断以后,所得到的各节木棍的长度。

在最后一组数据之后,是一个零。

输出格式

为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

数据范围

数据保证每一节木棍的长度均不大于 50。

输入样例:

9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0

输出样例

6
5

思路详解

代码实现

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N  = 70;
int n,length,sum;
int w[N];
bool st[N];
bool dfs(int u,int curlen,int start){
    if(u * length == sum) return true;
    if(curlen == length) return dfs(u+1,0,0);
    for(int i = start;i < n;i++){
        if(st[i]) continue;
        if(curlen + w[i] > length) continue;
        st[i] = true;
        if (dfs(u, curlen + w[i], i + 1)) return true;
        st[i] = false;
        
        if (!curlen || curlen + w[i] == length) return false;
        
        int j = i;
        while(j < n && w[j] == w[i]) j++;
        i = j-1;
    }
    return false;
}
int main(){
    while(cin>>n,n){
        memset(st,0,sizeof st);
        sum = 0;
        for(int i = 0;i < n; i++){
            cin>>w[i];
            sum+=w[i];
        }
        sort(w,w+n);
        reverse(w,w+n);
        length = 1;
        for(;length<=sum;length++){
            if(sum % length == 0 && dfs(0,0,0)){
                cout<<length<<endl;
                break;
            }
        }
    }
    return 0;
}

迭代加深

加成序列

题目描述

满足如下条件的序列 X(序列中元素被标号为 1、2、3…m)被称为“加成序列”:

X[1]=1
X[m]=n
X[1]<X[2]<…<X[m−1]<X[m]
对于每个 k(2≤k≤m)都存在两个整数 i 和 j (1≤i,j≤k−1,i 和 j 可相等),使得 X[k]=X[i]+X[j]。
你的任务是:给定一个整数 n,找出符合上述条件的长度 m 最小的“加成序列”。

如果有多个满足要求的答案,只需要找出任意一个可行解。

输入格式

输入包含多组测试用例。

每组测试用例占据一行,包含一个整数 n。

当输入为单行的 0 时,表示输入结束。

输出格式

对于每个测试用例,输出一个满足需求的整数序列,数字之间用空格隔开。

每个输出占一行。

数据范围

1≤n≤100

输入样例:

5
7
12
15
77
0

输出样例:

1 2 4 5
1 2 4 6 7
1 2 4 8 12
1 2 4 5 10 15
1 2 4 8 9 17 34 68 77

代码描述

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 110;
int n,depth;
int path[N];
bool st[N];
bool dfs(int u,int k){
    if(u == k) return path[u-1] == n;
    memset(st,0,sizeof st);
    for(int i = u-1;i >= 0; i--){
        for(int j = i;j>=0;j--){
            int tem = path[i] + path[j];
            if(tem > n || tem <= path[u-1] || st[tem]) continue;
            st[tem] = true;
            path[u] = tem;
            if(dfs(u+1,k)) return true;
        }
    }
    return false;
    
}
int main(){
    while(cin>>n,n){
        path[0] = 1;
        depth = 1;
        while(!dfs(1,depth)) depth++;
        for(int i = 0 ;i < depth; i++) cout<<path[i]<<' ';
        cout<<endl;
    }
    return  0;
}

双向DFS

送礼物

题目描述

达达帮翰翰给女生送礼物,翰翰一共准备了 N 个礼物,其中第 i 个礼物的重量是 G[i]。

达达的力气很大,他一次可以搬动重量之和不超过 W 的任意多个物品。

达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。

输入格式

第一行两个整数,分别代表 W 和 N。

以后 N 行,每行一个正整数表示 G[i]。

输出格式

仅一个整数,表示达达在他的力气范围内一次性能搬动的最大重量。

数据范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HhFjMweZ-1641720141175)(https://g.yuque.com/gr/latex?1%E2%89%A4N%E2%89%A446%2C%0A1%E2%89%A4W%2CG%5Bi%5D%E2%89%A4%202%20%5E31%5E-1#card=math&code=1%E2%89%A4N%E2%89%A446%2C%0A1%E2%89%A4W%2CG%5Bi%5D%E2%89%A4%202%20%5E31%5E-1&id=d0c0c33f)]

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N  = 1<<25;
int max_w,n,k;
int cnt = 1;
int ans;//答案
int g[50];
int weight[N];
void dfs1(int u ,int sum){
    if(u == k) {
        weight[cnt++] = sum;
        return;
    }
    dfs1(u+1,sum);
    if((long long)sum + g[u] <= max_w){
        dfs1(u+1,sum+g[u]);
    }
    
}
void dfs2(int u ,int sum){
    if(u == n){
        int l = 0, r = cnt-1;
        while(l < r){
            int mid  = l + r + 1 >>1;
            if((long long)weight[mid] +sum <= max_w){
                l = mid;
            }else{
                r = mid -1;
            }
        }
        ans = max(ans,weight[l] + sum);
        return;
    }
    dfs2(u+1,sum);
    if((long long)sum+g[u]<=max_w){
        dfs2(u+1,sum+g[u]);
    }
    
}
int main(){
    
    cin>>max_w>>n;
    for(int i = 0 ;i < n; i++) cin>>g[i];
    
    sort(g,g+n);
    reverse(g,g+n);
    
    k = n /2 +2;//将前k个物品的重量打一个表
    dfs1(0,0);
    
    
    sort(weight,weight+cnt);
    cnt = unique(weight,weight+cnt) - weight;
    
    dfs2(k,0);
    
    cout<<ans<<endl;
    
    return 0;
}

IDA*

排书

题目描述

给定 n 本书,编号为 1∼n。

在初始状态下,书是任意排列的。

在每一次操作中,可以抽取其中连续的一段,再把这段插入到其他某个位置。

我们的目标状态是把书按照 1∼n 的顺序依次排列。

求最少需要多少次操作。

输入格式

第一行包含整数 T,表示共有 T 组测试数据。

每组数据包含两行,第一行为整数 n,表示书的数量。

第二行为 n 个整数,表示 1∼n 的一种任意排列。

同行数之间用空格隔开。

输出格式

每组数据输出一个最少操作次数。

如果最少操作次数大于或等于 5 次,则输出 5 or more。

每个结果占一行。

数据范围

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zyS2m4E4-1641720141176)(https://g.yuque.com/gr/latex?1%E2%89%A4n%E2%89%A415#card=math&code=1%E2%89%A4n%E2%89%A415&id=a7a58ac0)]

思路详解

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N  = 15;
int T,n;
int q[N];
int w[5][N];
int f(){
    int total = 0;
    for(int i = 0 ;i < n-1; i ++){
        if(q[i] + 1 != q[i+1]) total++;
    }
    return (total + 2) / 3;
}
bool check(){
    for(int i = 0 ;i < n; i++){
        if(q[i]!=i+1){
            return false;
        }
    }
    return true;
}
bool dfs(int u ,int max_depth){
    if(u + f() > max_depth) return false;
    if(check()) return true;
    
    for(int len = 1; len <= n; len++){
        for(int l = 0; l+len-1 <n;l++){
            int r = l+len-1;
            for (int k = r + 1; k < n; k ++ )
            {
                memcpy(w[u], q, sizeof q);
                int x, y;
                for (x = r + 1, y = l; x <= k; x ++, y ++ ) q[y] = w[u][x];
                for (x = l; x <= r; x ++, y ++ ) q[y] = w[u][x];
                if (dfs(u+ 1, max_depth)) return true;
                memcpy(q, w[u], sizeof q);
            }
        }
    }
    return false;
}
int main(){
    cin>>T;
    while(T--){
        cin>>n;
        for(int i = 0 ;i < n; i++) cin>>q[i];
        int depth = 0;
        while(depth <5 && !dfs(0,depth)) depth++;
        if(depth >= 5) cout<<"5 or more"<<endl;
        else cout<<depth<<endl;
    }
    return 0;
}

回转游戏

题意描述

如下图所示,有一个 # 形的棋盘,上面有 1,2,3 三种数字各 8 个。

给定 8 种操作,分别为图中的 A∼H。

这些操作会按照图中字母和箭头所指明的方向,把一条长为 7 的序列循环移动 1 个单位。

例如下图最左边的 # 形棋盘执行操作 A 后,会变为下图中间的 # 形棋盘,再执行操作 C 后会变成下图最右边的 # 形棋盘。

给定一个初始状态,请使用最少的操作次数,使 # 形棋盘最中间的 8 个格子里的数字相同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6hmPMG7k-1641720141177)(https://cdn.jsdelivr.net/gh/freedom0123/img/Acwing181.jpg#id=So3KE&originHeight=200&originWidth=528&originalType=binary&ratio=1&status=done&style=none)]

输入格式

输入包含多组测试用例。

每个测试用例占一行,包含 24 个数字,表示将初始棋盘中的每一个位置的数字,按整体从上到下,同行从左到右的顺序依次列出。

输入样例中的第一个测试用例,对应上图最左边棋盘的初始状态。

当输入只包含一个 0 的行时,表示输入终止。

输出格式

每个测试用例输出占两行。

第一行包含所有移动步骤,每步移动用大写字母 A∼H 中的一个表示,字母之间没有空格,如果不需要移动则输出 No moves needed。

第二行包含一个整数,表示移动完成后,中间 8 个格子里的数字。

如果有多种方案,则输出字典序最小的解决方案。

输入样例:

1 1 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3
1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
0

输出样例:

AC
2
DDHH
2

思路

代码

#include<iostream>
#include<cstring>
using namespace std;
const int N  = 25;
int path[100];
int q[N];
int option[8][8] = {
    {0, 2, 6, 11, 15, 20, 22},
    {1, 3, 8, 12, 17, 21, 23},
    {10, 9, 8, 7, 6, 5, 4},
    {19, 18, 17, 16, 15, 14, 13},
    {23, 21, 17, 12, 8, 3, 1},
    {22, 20, 15, 11, 6, 2, 0},
    {13, 14, 15, 16, 17, 18, 19},
    {4, 5, 6, 7, 8, 9, 10}
};
int opposite[8] = {5,4,7,6,1,0,3,2};//每个操作的逆过程
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};//中间的8个格子
int f(){
    int sum[4];
    memset(sum,0,sizeof sum);
    for(int i = 0 ;i < 8;i++) sum[q[center[i]]]++;
    int s = 0;
    for(int i = 0 ;i < 4; i++) s = max(s,sum[i]);
    return 8 - s;
}
bool check(){
    int st = q[center[0]];
    for(int i = 0 ;i < 8;i++) {
        if(q[center[i]] != st){
            return false;
        }
    }
    return true;
}
void action(int x)
{
    int t = q[option[x][0]];
    for (int i = 0; i < 6; i ++ ) q[option[x][i]] = q[option[x][i + 1]];
    q[option[x][6]] = t;
}

bool dfs(int u,int max_depth,int last){
    if(u + f() > max_depth) return false;
    if(check()) return true;
    for(int i = 0 ;i < 8 ; i++){
        if(opposite[i] == last) continue;
        action(i);
        path[u] = i;
        if(dfs(u+1,max_depth,i)) return true;
        action(opposite[i]);
        
    }
    return false;
    
}
int main(){
    while(cin>>q[0] && q[0]){
        for(int i = 1; i < 24 ; i++) cin>>q[i];
        int depth = 0;
        while(!dfs(0,depth,-1)) depth++;
        if(!depth) cout<<"No moves needed"<<endl;
        else{
            for(int i = 0; i < depth; i++){
                printf("%c",path[i]+'A');
            }
            cout<<endl;
        }
        cout<<q[6]<<endl;
    }
    return 0;
}

leetcode

605.岛屿的最大面积

题目描述

给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

思路

这题就是DFS的连通块模型,本质上就是寻找最大的连通块的大小

代码描述

class Solution {
    int N  = 55;
    int n,m;
    int dx[] = new int[] {0,-1,0,1};
    int dy[] = new int[] {-1,0,1,0};
    boolean[][] st  = new boolean[N][N];
    public int maxAreaOfIsland(int[][] grid) {
        int ans = 0;
        n = grid.length;
        m = grid[0].length;
        for(int i = 0;i < n;i++){
            for(int j  = 0;j < m;j++){
                if(grid[i][j]!=0){
                    ans = Math.max(ans,dfs(i,j,grid));
                }
            }
        }
        return ans;
    }
    public int  dfs(int x,int y,int[][]grid){
        int res  = 1;
        st[x][y] = true;
        for(int  i = 0;i < 4;i++){
            int a = x + dx[i];
            int b = y + dy[i];
            if(a < 0 || a>=n || b<0 || b>=m || grid[a][b]==0) continue;
            if(st[a][b]) continue;
            res += dfs(a,b,grid);
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

佩奇inging

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

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

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

打赏作者

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

抵扣说明:

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

余额充值