DFS

深度优先搜索(DFS)

DFS介绍

深搜,就是在每个点 x 上面对多条分支时,任意选择一条边走下去,执行递归,直至回溯到点 x 后,再考虑走其他的边。

树的DFS序、深度和重心

这个很简单,不写了

深搜剪枝

  1. 优化搜索顺序

    大部分情况下,我们应该优先搜索分支较少的节点。

  2. 排除等效冗余

    在搜索的时候,尽量不搜索重复的状态,即在不考虑顺序的时候,尽量用组合的方式来搜索。

  3. 可行性剪枝

    搜索的一半的时候,发现不合法就可以退出。

  4. 最优性剪枝

    搜索到某一个程度的时候,如果发现当前的状态无论如何都比之前搜索到的最优解差,那就可以退出。

  5. 记忆化搜索(DP)

例题

小猫爬山(CH2201)

题目描述:

翰翰和达达饲养了 N N N 只小猫,这天,小猫们要去爬山。
经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。
翰翰和达达只好花钱让它们坐索道下山。
索道上的缆车最大承重量为 W W W,而 N N N 只小猫的重量分别是 C 1 、 C 2 … … C N C_1、C_2……C_N C1C2CN
当然,每辆缆车上的小猫的重量之和不能超过 W W W
每租用一辆缆车,翰翰和达达就要付 1 1 1 美元,所以他们想知道,最少需要付多少美元才能把这 N N N 只小猫都运送下山?

思路:

对于这题,我们如果用深搜来做,可以考虑依次把每只小猫分配到一辆已经租好的缆车上,或者新租一辆给小猫。

此时,我们只关心三个状态:

  1. 已经在缆车上的小猫有几只
  2. 已经租用的缆车的数量
  3. 每辆缆车上当前搭载的小猫重量之和

根据这三个状态,我们可以考虑编写一个 d f s ( n o w , c n t ) dfs(now, cnt) dfs(now,cnt) 函数,其中 n o w now now 代表当前即将运送的小猫, c n t cnt cnt 代表已经租用的缆车数量,还可以写一个 c a b cab cab 数组来存储每辆缆车上小猫的重量之和。

n o w now now c n t cnt cnt c a b cab cab 数组共同标识着问题状态空间所类比的“图”中的一个“节点”。在这个“节点”上,至多有 c n t + 1 cnt + 1 cnt+1 个可能的分支。

于是,我们可以只考虑当前这个 n o w now now 小猫是否能够被分配到已租用的缆车上,条件为 c a b [ i ] + c [ n o w ] ≤ W cab[i] + c[now] \le W cab[i]+c[now]W 。如果不能,那么就多租一辆来分配这个小猫, c a b [ c n t + 1 ] = c [ n o w ] cab[cnt + 1] = c[now] cab[cnt+1]=c[now]

为了减少搜索树“分支”的数量,我们可以在搜索前,先把小猫按照重量从大到小递减排序,优先搜索重量较大的小猫。

#include 
#include 
#include 

using namespace std;

const int N = 20;

int n, w;
int c[N], cab[N];
int ans = 0;

bool cmp(int a, int b)
{
    return a > b;
}

void dfs(int now, int cnt)
{
    if(cnt >= ans) return ;
    if(now == n) {
        ans = min(ans, cnt);
        return ;
    }
    
    for(int i = 1; i <= cnt; i++)
        if(c[now] + cab[i] <= w) {
            cab[i] += c[now];
            dfs(now + 1, cnt);
            cab[i] -= c[now];
        }
    
    cab[cnt + 1] = c[now];
    dfs(now + 1, cnt + 1);
    cab[cnt + 1] = 0;
    return ;
}

int main()
{
    cin >> n >> w;
    for(int i = 0; i < n; i++) cin >> c[i];
    
    sort(c, c + n, cmp);
    ans = 0x3f3f3f3f;
    dfs(0, 0);
    
    cout << ans << endl;
    
    return 0;
}
Sudoku(POJ2676/3074)

题目描述:

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

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

输入格式

输入包含多组测试用例。

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

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

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

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

输出格式

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

输入样例:

4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end

输出样例:

417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936

思路

本题的“搜索树”规模是非常大的,如果直接随便找一个可以填的位置就去搜索的话,肯定是不行的。

于是这里考虑一个剪枝,就是选择“能填的合法数字”最少的位置,并且考虑该位置应填上什么数,作为搜索分支。

但是对于这样一长串的数组,如果直接搜索也是不好做的,可以考虑位运算来代替数组执行“对数独各个位置所填数字的记录”以及“可填性的检查与统计”,也就是进行了常数优化

  1. 对于每行、每列和每个 3 ∗ 3 3*3 33 九宫格,分别用一个 9 9 9 位二进制数保存状态,表示哪些数字还可填。
  2. 对于每个位置,把它所在的行、列和九宫格的二进制数做按位与( & \& &)运算,就能得到当前位置能填哪些数,用 l o w b i t lowbit lowbit 运算就可以把能填的数字取出。
  3. 当一个位置填上某个数之后,把该位置所在的行、列、九宫格记录的二进制数的对应位更新为 0 0 0;回溯时改回 1 1 1 即可还原现场。

代码实现

#include 
#include 
#include 

using namespace std;

const int N = 9, M = 1 << N;

int ones[M], map[M];
int row[N], col[N], cell[N][N];
char str[100];

//取出最末尾的1,取出来的数是一个十进制的整数。
inline int lowbit(int x)
{
    return x & -x;
}

//将行、列和3*3九宫格里的状态都做一次按位与运算,得出哪些位置上可以放数字。
inline int get(int x, int y)
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}

//初始化,最开始的时候所有状态都是全1,表示都可以放置数字
void init()
{
    for(int i = 0; i < N; i++) row[i] = col[i] = M - 1;
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++)
            cell[i][j] = M - 1;
}

//将(x,y)填上对应的数字 1 + t
//is_set 为 true 表示填数字,false 表示还原成 '.'
void draw(int x, int y, int t, bool is_set)
{
    if(is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';
    
    int v = 1 << t;
    if(!is_set) v = -v;
    
    row[x] -= v;
    col[y] -= v;
    cell[x / 3][y / 3] -= v;
}

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 * N + j] == '.')
            {
                int t = ones[get(i, j)];
                if(t < minv)
                {
                    minv = t;
                    x = i, y = j;
                }
            }
    
    //分别搜索每一个可填数
    int state = get(x, y);
    for(int i = state; i; i -= lowbit(i))
    {
        int t = map[lowbit(i)];
        draw(x, y, t, true);
        if(dfs(cnt - 1)) return true;
        draw(x, y, t, false);
    }
    
    return false;
}

int main()
{
    //ones[i]表示数i中有几个1
    for(int i = 0; i < M; i++)
        for(int j = 0; j < N; j++)
            ones[i] += i >> j & 1;
    
    //将数 10^i 中1的位置预处理出来
    for(int i = 0; i < N; i++) map[1 << i] = i;
    
    while(scanf("%s", str) != EOF && 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';
                    draw(i, j, t, true);
                }
                else cnt ++;
                
        dfs(cnt);
        
        printf("%s\n", str);
    }
    return 0;
}
Sticks(POJ1011)

题目描述

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

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

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

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

输入格式

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

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

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

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

输出格式

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

数据范围

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

输入样例:

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

输出样例:

6
5

代码实现

#include 
#include 
#include 

using namespace std;

const int N = 65;

int n;
int l[N], sum, length;
bool st[N];

bool dfs(int u, int s, int start)
{
    if(u * length == sum) return true;
    if(s == length) return dfs(u + 1, 0, 0);
    
    for(int i = start; i < n; i++)
    {
        if(st[i]) continue;
        if(s + l[i] > length) continue;
        
        st[i] = true;
        if(dfs(u, s + l[i], i + 1)) return true;
        st[i] = false;
        
        if(!s) return false;
        if(s + l[i] == length) return false;
        
        int j = i;
        while(j < n && l[i] == l[j]) j++;
        i = j - 1;
    }
    
    return false;
}

int main()
{
    while(cin >> n, n)
    {
        memset(st, false, sizeof st);
        sum = 0;
        
        for(int i = 0; i < n; i++)
        {
            cin >> l[i];
            sum += l[i];
        }
        
        sort(l, l + n);
        reverse(l, l + n);
        
        length = 1;
        while(true)
        {
            if(sum % length == 0 && dfs(0, 0, 0))
            {
                cout << length << endl;
                break;
            }
            length ++;
            if(length > sum) break;
        }
    }
    return 0;
}
生日蛋糕(POJ1190)

题目描述:

7月17日是 M r . W Mr.W Mr.W 的生日, A C M − T H U ACM-THU ACMTHU 为此要制作一个体积为 N π N_π Nπ M M M 层生日蛋糕,每层都是一个圆柱体。
设从下往上数第 i ( 1 ≤ i ≤ M ) i(1 \le i \le M) i(1iM) 层蛋糕是半径为 R i R_i Ri , 高度为 H i H_i Hi 的圆柱。当 i < M i < M i<M 时,要求 R i > R i + 1 R_i > R_{i+1} Ri>Ri+1 H i > H i + 1 H_i > H_{i+1} Hi>Hi+1
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q Q Q 最小。
Q = S π Q = Sπ Q=Sπ
请编程对给出的 N N N M M M ,找出蛋糕的制作方案(适当的 R i R_i Ri H i H_i Hi 的值),使 S S S 最小。
(除 Q Q Q 外,以上所有数据皆为正整数)

思路:

本题没什么难以理解的。

这里设最顶上一层为第 1 1 1 层,共有 M M M 层,总体积为 N N N ,且有 R i < R i + 1 , H i < H i + 1 R_iRi<Ri+1,Hi<Hi+1

剪枝策略

  1. 上下界剪枝:

    u ≤ R [ u ] ≤ m i n ( R [ u + 1 ] − 1 , n − v ) u ≤ H [ u ] ≤ m i n ( H [ u + 1 ] − 1 , u − v R [ u ] 2 ) u\le R[u] \le min(R[u + 1]-1, \sqrt{n - v}) \\ u\le H[u] \le min(H[u+1]-1, \cfrac{u-v}{R[u]^2}) uR[u]min(R[u+1]1,nv )uH[u]min(H[u+1]1,R[u]2uv)

    证明:

    由于每一层 R R R H H H 的大小都是严格递增的,并且都为正整数,所以最小也只能是第 u u u 层的层数。

    对于体积 V u = π R u 2 H u = n − v V_u=\pi R_u^2 H_u = n-v Vu=πRu2Hu=nv

    所以,当 H u H_u Hu 1 1 1 时, R u 2 R_u^2 Ru2 可以取到最大。因此 R u ≤ m i n ( R u + 1 − 1 , n − v ) R_u\le min(R_{u+1}-1, \sqrt{n-v}) Rumin(Ru+11,nv )

    对于 H u H_u Hu 也是同理。

  2. 优化搜索顺序:

    每一层的 R R R H H H 都从大到小枚举,减少分支数量。

  3. 可行性/最优性剪枝:

    m i n v [ u ] minv[u] minv[u] 代表第 u u u 层前最小的体积之和, m i n s [ u ] mins[u] mins[u] 代表第 u u u 层前最小的面积之和。

    v + m i n v [ u ] ≤ n s + m i n s [ u ] < a n s v+minv[u] \le n \\ s+mins[u] < ans v+minv[u]ns+mins[u]<ans

    还有一个最优性剪枝,比较难想,这里需要一定的证明。

    利用 R R R 数组和 H H H 数组,1~u 层的体积可表示为 n − v = ∑ k = 1 u R [ k ] 2 H [ k ] n-v=\sum_{k=1}^{u}R[k]^2H[k] nv=k=1uR[k]2H[k],表面积可表示为 S u = ∑ k = 1 u 2 R [ k ] H [ k ] S_u=\sum_{k=1}^u2R[k]H[k] Su=k=1u2R[k]H[k]

    其中, S u = 2 ∑ k = 1 u R [ k ] H [ k ] = 2 R [ u + 1 ] ∑ k = 1 u R [ k ] H [ k ] R [ u + 1 ] > 2 R [ u + 1 ] ∑ k = 1 u R [ k ] 2 H [ k ] S_u=2\sum_{k=1}^uR[k]H[k]= \cfrac{2}{R[u+1]}\sum_{k=1}^uR[k]H[k]R[u+1]>\cfrac{2}{R[u+1]}\sum_{k=1}^uR[k]^2H[k] Su=2k=1uR[k]H[k]=R[u+1]2k=1uR[k]H[k]R[u+1]>R[u+1]2k=1uR[k]2H[k]

    所以, S u > 2 ( n − v ) R [ u + 1 ] S_u>\cfrac{2(n-v)}{R[u+1]} Su>R[u+1]2(nv)

    因此,当 s + S u = s + 2 ( n − v ) R [ u + 1 ] ≥ a n s s+S_u=s+\cfrac{2(n-v)}{R[u+1]} \ge ans s+Su=s+R[u+1]2(nv)ans 的时候,就可以剪枝了。

代码实现:

#include 
#include 
#include 
#include 
#include 

using namespace std;

const int N = 25;
const int INF = 1e9;

int n, m;
int minv[N], mins[N];
int R[N], H[N];
int ans = INF;

void dfs(int u, int v, int s)
{
    if(v + minv[u] > n) return ;
    if(s + mins[u] >= ans) return ;
    if(s + 2 * (n - v) / R[u + 1] >= ans) return ;
    
    if(!u)
    {
        if(v == n) ans = s;
        return ;
    }
    
    for(int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r--)
        for(int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h--)
        {
            int t = 0;
            if(u == m) t = r * r;
            R[u] = r, H[u] = h;
            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}

int main()
{
    scanf("%d %d", &n, &m);
    
    for(int i = 1; i <= m; i++)
    {
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }
    
    R[m + 1] = H[m + 1] = INF;
    
    dfs(m, 0, 0);
    
    if(ans == 1e9) ans = 0;
    printf("%d\n", ans);
    
    return 0;
}

迭代加深

迭代加深

例题
Addition Chains(POJ2248)
#include 
#include 

using namespace std;

const int N = 110;
int n;
int path[N];

bool dfs(int u, int depth)
{
    if(u > depth) return false;
    if(path[u - 1] == n) return true;
    
    bool st[N] = {0};
    
    for(int i = u - 1; i >= 0; i--)
        for(int j = i; j >= 0; j--)
        {
            int s = path[i] + path[j];
            if(s > n || st[s] || s <= path[u - 1]) continue;
            
            st[s] = true;
            path[u] = s;
            if(dfs(u + 1, depth)) return true;
        }
        
    return false;
}

int main()
{
    path[0] = 1;
    while(cin >> n, n)
    {
        int depth = 1;
        while(!dfs(1, depth)) depth ++;
        for(int i = 0; i < depth; i++) cout << path[i] << " ";
        cout << endl;
    }
    return 0;
}

双向搜索

例题
送礼物(POJ2401)

题目描述

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

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

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ N ≤ 46 , 1 ≤ W , G [ i ] ≤ 2 31 − 1 1≤N≤46, 1≤W,G[i]≤2^{31}−1 1N46,1W,G[i]2311

输入样例:

20 5
7
5
4
18
1

输出样例:

19

代码实现

#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;
const int N = 50;

int n, m;
int w[N], k;
int weight[1 << 25], cnt;
int ans;

void dfs1(int u, int s)
{
    if(u == k)
    {
        weight[cnt ++] = s;
        return ;
    }
    dfs1(u + 1, s);
    if((LL)s + w[u] <= m) dfs1(u + 1, s + w[u]);
}

void dfs2(int u, int s)
{
    if(u == n)
    {
        int l = 0, r = cnt - 1;
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            if(weight[mid] <= m - s) l = mid;
            else r = mid - 1;
        }
        ans = max(ans, weight[l] + s);
        return ;
    }
    dfs2(u + 1, s);
    if((LL)s + w[u] <= m) dfs2(u + 1, s + w[u]);
}

int main()
{
    cin >> m >> n;
    for(int i = 0; i < n; i++) cin >> w[i];
    
    sort(w, w + n);
    reverse(w, w + n);
    k = n / 2 + 2;
    dfs1(0, 0);
    
    sort(weight, weight + cnt);
    cnt = unique(weight, weight + cnt) - weight;
    
    dfs2(k, 0);
    
    cout << ans << endl;
    
    return 0;
}

连通性模型

通常,连通性模型其实dfs和bfs都是可以做的,其中区别就是“bfs在搜索状态的过程中,可以记录到达每个点的最短距离,而dfs只能确认是否能够到达”,由于dfs实现较快,所以也常采用dfs来解决一系列连通性的问题。

例题

迷宫

题目描述:

一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n ∗ n n∗n nn 的格点组成,每个格点只有2种状态,.#,前者表示可以通行后者表示不能通行。

同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。

如果起点或者终点有一个不能通行(为#),则看成无法办到。

注意:A、B不一定是两个不同的点。

代码实现:

#include 
#include 
#include 
#include 

using namespace std;

const int N = 110;

int n;
char g[N][N];
bool st[N][N];
int sx, sy, ex, ey;
int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};

bool dfs(int x, int y)
{
    if(g[x][y] == '#') return false;
    if(x == ex && y == ey) return true;

    st[x][y] = true;
    for(int i = 0; i < 4; i++)
    {
        int tx = x + dx[i], ty = y + dy[i];
        if(tx < 0 || tx >= n || ty < 0 || ty >= n) continue;
        if(st[tx][ty]) continue;
        if(dfs(tx, ty)) return true;
    }

    return false;
}

int main()
{
    int t;
    scanf("%d", &t);
    while(t--)
    {
        memset(st, false, sizeof st);
        scanf("%d", &n);
        for(int i = 0; i < n; i++) scanf("%s", g[i]);

        scanf("%d %d %d %d", &sx, &sy, &ex, &ey);

        if(dfs(sx, sy)) puts("YES");
        else puts("NO");
    }
    return 0;
}
红与黑

题目描述:

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

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

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

其实这题也就类似与 F l o o d − F i l l Flood-Fill FloodFill

代码实现:

#include 
#define sc scanf
#define pf printf
using namespace std;
typedef pair PII;
const int N = 25;

int n, m, cnt;
char mp[N][N];
bool vis[N][N];

void dfs(int x, int y)
{
    if(x < 0 || x >= n || y < 0 || y >= m) return ;
    if((mp[x][y] == '.' || mp[x][y] == '@') && !vis[x][y]) {
        cnt++;
        vis[x][y] = true;
        dfs(x - 1, y);
        dfs(x + 1, y);
        dfs(x, y - 1);
        dfs(x, y + 1);
    }
}

int main()
{
    while(cin >> m >> n && n && m)
    {
        memset(vis, false, sizeof vis);
        cnt = 0;
        int stx = 0, sty = 0;
        for(int i = 0; i < n; i++)
            for(int j = 0; j < m; j++)
            {
                cin >> mp[i][j];
                if(mp[i][j] == '@')
                {
                    stx = i;
                    sty = j;
                }
            }
        // vis[stx][sty] = true;
        dfs(stx, sty);
        cout << cnt << endl;
    }
    return 0;
}

你可能感兴趣的:(搜索,剪枝,dfs,算法)