DFS的理解和應用

目錄

DFS(Depth First Search)

數塔問題

Prime Ring Problem - HDOJ 1016 / UVa 524 /(紫書P194例題7-4)

Zipper HDOJ - 1501(DFS+剪枝)

Lake Counting POJ - 2386

棋盤問題 POJ - 1321

水果消除 HNUSTOJ 

團隊程序設計天梯賽--L3-015 球隊“食物鏈”

數獨挑戰


DFS(Depth First Search)

算法競賽中的一個重要技巧,在許多題目裏,用DFS有着神奇的作用。

利用棧這種數據結構來實現(找到的第一個解不一定是最優解,只是先序遍歷最早的可行解)

案例解釋:走迷宮

看到哪個方向可以走就走哪個,而且你沒有辦法分身,所以只能慢慢試探,不撞南牆不回頭。


數塔問題

題目描述:

輸入一個三角形塔,從三角塔頂出發向下走,每個點都有不同的權值,走到那個點就獲得對應的權值,求走到塔底的時候能夠獲得的權值的最大值。

Sample Input

4

5

8 4

3 6 9

7 2 9 5

Sample Output

28

評論區反映這個塔不知道是怎麼走的,這個塔其實就是這樣的

每一個點只能走下屬的兩個分支的其中一個,所以,DFS的路徑是5→8→6→9,5+8+6+9=28.

先來一個最普通的代碼。

///數塔 1.0
#include <stdio.h>
#include <iostream>
#include <set>
#include <cmath>
#include <sstream>
#define MAX 100010
using namespace std;

int n;
int a[200][200];
int DFS(int i,int j)
{
    if(i==n)
        return a[i][j];
    int x = DFS(i+1,j);
    int y = DFS(i+1,j+1);
    return max(x,y) + a[i][j];
}

int main()
{
    while(cin>>n)
    {
        for(int i=1;i<=n;i++)
            for(int j=1;j<=i;j++)
                cin>>a[i][j];
        cout<<DFS(1,1)<<endl;
    }
    return 0;
}

這個做法很直觀,但是每一次都要從頭開始搜,有大量的重複計算,這樣非常耗時,需要優化。

可以另外加一個vis數組來存儲上一次計算的結果,這樣就可以節省計算時間。

///數塔 2.0
#include <stdio.h>
#include <iostream>
#include <set>
#include <cmath>
#include <cstring>
#include <sstream>
#define MAX 100010
using namespace std;

int n;
int a[200][200];
int vis[200][200];
int DFS(int i,int j)
{
    if(vis[i][j]!=-1)
        return vis[i][j];
    if(i==n)///end of this road
        vis[i][j] = a[i][j];///save the result
    else
    {
        int x = DFS(i+1,j);
        int y = DFS(i+1,j+1);
        vis[i][j] = max(x,y) + a[i][j];
    }
    return vis[i][j];
}

int main()
{
    while(cin>>n)
    {
        memset(a,0,sizeof(a));
        memset(vis,0xff,sizeof(vis));
        for(int i=1;i<=n;i++)
            for(int j=1;j<=i;j++)
                cin>>a[i][j];
        cout<<DFS(1,1)<<endl;
    }
    return 0;
}

Prime Ring Problem - HDOJ 1016 / UVa 524 /(紫書P194例題7-4)

題目描述:輸入一個正整數n,把整數1,2,3,...,n組成一個環,使得相鄰兩個整數之和均爲素數,輸出時從整數1開始逆時針排列。同一個環輸出一次。(n<=16)

Sample Input

6 8

Sample Output

Case 1:
1 4 3 2 5 6
1 6 5 2 3 4


Case 2:
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

分析:最大的數是16,如果把所有可能結果都生成然後一個個試。。。肯定超時。

這裏可以用DFS回溯,即用深度優先遍歷解答樹(可參考紫書的解釋)

另外注意輸出格式,案例之間有一個空行

///Prime Ring Problem
#include <cstdio>
#include <iostream>
#include <cmath>
using namespace std;

int vis[25]={0};
int n,num[25]={0};

int is_prime(int k)///判斷素數,也可以打表,那樣更快
{
    int i;
    for(i=2;i<=sqrt(k);i++)
        if(k%i==0)
        return 0;
    return 1;
}
void DFS(int k)
{
    int i;
    if(k>n&&is_prime(num[n]+num[1]))///測試最後一個數和第一個數之和是否爲素數
    {
        for(i=1;i<n;i++)
            printf("%d ",num[i]);
        printf("%d\n",num[i]);
    }
    else
    {
        for(i=2;i<=n;i++)///嘗試放置每個數i
        {
            if(!vis[i]&&is_prime(i+num[k-1]))///i沒有用過且與前一個數之和爲素數
            {
                vis[i]=1;
                num[k]=i;
                DFS(k+1);
                vis[i]=0;
            }
        }
    }
}
int main()
{
    int cnt=1;
    while(cin>>n)
    {
        if(n<1||n>19)
            break;
        printf("Case %d:\n",cnt++);
        num[1]=1;
        DFS(2);
        printf("\n");
    }
    return 0;
}

Zipper HDOJ - 1501(DFS+剪枝)

題目描述:給出兩個字符串,問能否在不改變字符串本身順序的情況下,拆開重組成指定字符串,輸出yes/no。

Sample Input

3 
cat tree tcraete 
cat tree catrtee 
cat tree cttaree

Sample Output

Data set 1: yes 
Data set 2: yes 
Data set 3: no

 

( ˙˘˙ )沒想到吧,這題居然也能用DFS。

分析:從三個串的首元素開始,遇到一串/二串與目標串匹配的字母,繼續向下DFS,如果這些可以到達目標串的尾元素,那麼意味着前面的都匹配成功,所以這個結果是yes,否則no。

//Zipper HDOJ 1501
#include <cstdio>
#include <iostream>
using namespace std;

char a[209],b[209],c[409];
int vis[209][209]={0};
int OK=0;
void DFS(int i,int j,int k)
{
    if(vis[i][j]==1)///已經訪問過,剪掉
        return;
    if(c[k]=='\0')///到指定字符串的結尾,說明之前的都匹配成功
    {
        OK=1;
        return;
    }
    if(a[i]!='\0'&&c[k]==a[i])//匹配成功
        DFS(i+1,j,k+1);
    if(b[j]!='\0'&&c[k]==b[j])//匹配成功
        DFS(i,j+1,k+1);
    vis[i][j]=1;///cut
}
int main()
{
    int n,i,j,cnt=0;
    cin>>n;
    while(n--)
    {
        OK=0;
        for(i=0;i<209;i++)///cut
            for(j=0;j<209;j++)
            vis[i][j]=0;
        scanf("%s%s%s",a,b,c);
        DFS(0,0,0);
        printf("Data set %d: %s\n",++cnt,OK?"yes":"no");
    }
    return 0;
}

Lake Counting POJ - 2386

題目描述:給出一張n*m的地圖,求圖中水窪的個數。水窪的大小不定,如果W的四周也有W,這些W組成一個大的水窪。

Sample Input

10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.

Sample Output

3

分析:找到‘W’的地,然後對其四周進行判斷(DFS),並且把它附近全部變成‘.’,計算這樣做的次數,即爲池塘的個數。

//Lake Counting POJ2386
#include <cstdio>
#include <iostream>
#include <cmath>
using namespace std;
char farm[109][109];
int n,m;

void dfs(int x,int y)
{
    farm[x][y]='.';
    int i,j;
    int tx,ty;
    for(i=-1;i<=1;i++)
    {
        for(j=-1;j<=1;j++)
        {
            tx=x+i;
            ty=y+j;
            if((tx>=0&&tx<n)&&(ty>=0&&ty<m)&&farm[tx][ty]=='W')
                dfs(tx,ty);
        }
    }
}
int main(void)
{
    int i,j;
    while(cin>>n>>m)
    {
        int sum=0;
        getchar();
        for(i=0;i<n;i++)
        {
            for(j=0;j<m;j++)
                cin>>farm[i][j];
            getchar();
        }
        for(i=0;i<n;i++)
            for(j=0;j<m;j++)
            {
                if(farm[i][j]=='W')
                {
                    dfs(i,j);
                    sum++;
                }
            }
        cout<<sum<<endl;
    }
    return 0;
}

棋盤問題 POJ - 1321

題目描述:給出一個n*m的棋盤,‘#’可以放棋子,求滿足所有放下的棋子都不在同一行同一列的情況有多少種。(類似於八皇后問題)

Sample Input

2 1
#.
.#
4 4
...#
..#.
.#..
#...
-1 -1

Sample Output

2
1

分析:用DFS,滿足條件則往下一個位置繼續,如果放下的棋子已經達到要求的個數,則方法+1。

//POJ - 1321
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
///notice that the '#' can be placed
int n,m;
int ans=0;
char map[10][10];
int vis[10];
void DFS(int x,int step)
{
    if(step == m)///found a way that work
    {
        ans++;
        return;
    }
    if(x==n)
        return;
    DFS(x+1,step);
    for(int j=0; j<n; j++)
    {
        if(vis[j]==0 && map[x][j]=='#')///這行這列都沒放棋子
        {
            vis[j]=1;
            DFS(x+1,step+1);
            vis[j]=0;///for different situations
        }
    }
}
int main()
{
    while(cin>>n>>m)
    {
        if(n==-1 && m==-1)
            break;
        ans=0;
        memset(vis,0,sizeof(vis));
        for(int i=0; i<n; i++)
            scanf("%s",map[i]);
        DFS(0,0);
        printf("%d\n",ans);
    }
    return 0;
}

水果消除 HNUSTOJ 

題目描述:給出一個n*n的表格,表格元素是數字,相同的數字表示相同的水果,兩個或以上相鄰的水果可以消除,(消除之後其他水果不會改變位置),問可以消除的方案數。

Sample Input

6
1 1 2 2 2 2
1 3 2 1 1 2
2 2 2 2 2 3
3 2 3 3 1 1
2 2 2 2 3 1
2 3 2 3 2 2

Sample Output

6

分析:用DFS找某個數字周圍的相同數字,如果相同就沿着這個數字向下繼續找,如果相同的數字個數大於等於2,那麼這樣的一次搜索就是一個方案,搜索過的數字變成0,代表已經搜索過,最終輸出方案數。

///水果消除 HNUSTOJ
#include <cstdio>
#include <iostream>
using namespace std;
///notice that the '#' can be placed while '.' not
int map[1000][1000];
int dir[4][2]= {{1,0},{-1,0},{0,1},{0,-1}};
int n;
int num;
void DFS(int x,int y,int k)
{
    map[x][y]=0;
    int i;
    int tx,ty;
    for(i=0;i<4;i++)
    {
        tx=x+dir[i][0];
        ty=y+dir[i][1];
        if(tx>=0 && tx<n && ty>=0 && ty<n && (map[tx][ty] == k))
        {
            num++;
            DFS(tx,ty,k);
        }
    }
}
int main()
{
    while(cin>>n)
    {
        int sum=0;
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                scanf("%d",&map[i][j]);
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                if(map[i][j])
                {
                    num=1;
                    DFS(i,j,map[i][j]);
                    if(num>1)
                         sum++;
                }
        printf("%d\n",sum);
    }
    return 0;
}

團隊程序設計天梯賽--L3-015 球隊“食物鏈”

題目鏈接

某國的足球聯賽中有N支參賽球隊,編號從1至N。聯賽採用主客場雙循環賽制,參賽球隊兩兩之間在雙方主場各賽一場。

聯賽戰罷,結果已經塵埃落定。此時,聯賽主席突發奇想,希望從中找出一條包含所有球隊的“食物鏈”,來說明聯賽的精彩程度。“食物鏈”爲一個1至N的排列{ T​1​​ T​2​​ ⋯ T​N​​ },滿足:球隊T​1​​戰勝過球隊T​2​​,球隊T​2​​戰勝過球隊T​3​​,⋯,球隊T​(N−1)​​戰勝過球隊T​N​​,球隊T​N​​戰勝過球隊T​1​​。

現在主席請你從聯賽結果中找出“食物鏈”。若存在多條“食物鏈”,請找出字典序最小的。

注:排列{ a​1​​ a​2​​ ⋯ a​N​​}在字典序上小於排列{ b​1​​ b​2​​ ⋯ b​N​​ },當且僅當存在整數K(1≤K≤N),滿足:a​K​​<b​K​​且對於任意小於K的正整數i,a​i​​=b​i​​。

輸入格式:

輸入第一行給出一個整數N(2≤N≤20),爲參賽球隊數。隨後N行,每行N個字符,給出了N×N的聯賽結果表,其中第i行第j列的字符爲球隊i在主場對陣球隊j的比賽結果:W表示球隊i戰勝球隊j,L表示球隊i負於球隊j,D表示兩隊打平,-表示無效(當i=j時)。輸入中無多餘空格。

輸出格式:

按題目要求找到“食物鏈”T​1​​ T​2​​ ⋯ T​N​​,將這N個數依次輸出在一行上,數字間以1個空格分隔,行的首尾不得有多餘空格。若不存在“食物鏈”,輸出“No Solution”。

輸入樣例1:

5
-LWDW
W-LDW
WW-LW
DWW-W
DDLW-

輸出樣例1:

1 3 5 4 2

輸入樣例2:

5
-WDDW
D-DWL
DD-DW
DDW-D
DDDD-

輸出樣例2:

No Solution

分析:DFS+剪枝,詳細見代碼註釋~

///球隊食物鏈
#include<bits/stdc++.h>

using namespace std;
const int maxn = 30;
char ar[maxn][maxn];
int N;
int  w[maxn][maxn];
bool vis[maxn];
int  ans[maxn];
bool dfs(int i,int num)
{
    if(num == N && w[i][0])///都走到這裏了,答案肯定已經確定了
    {
        for(int k = 0; k < N; ++k)///輸出結果
        {
            if(k)
                cout<<' ';
            cout<<ans[k]+1;
        }
        return true;
    }

// 遍歷,如果不能和0 形成環,就停止搜索
    int k;
    for( k = 1;k < N; ++k)
    {
        if(!vis[k] && w[k][0])
            break;
    }
    if(k >= N)///怎麼都不能形成環
        return false;



    for(int j = 1; j < N; ++j)
    {
        if(!vis[j]&&w[i][j])
        {
            ans[num] = j;
            vis[j] = 1;
            if(dfs(j,num+1))
                return true;
            vis[j] = false;
        }
    }
    return false;
}

int main()
{
    ios::sync_with_stdio(false);
    cin>>N;
    for(int i = 0; i < N; ++i)
        cin>>ar[i];
    for(int i = 0; i < N; ++i)
    {
        for(int j = 0; j < N; ++j)
        {
            if(i == j)
                continue;
            if(ar[i][j] =='W')
                w[i][j] = 1;
            else if(ar[i][j] == 'L')
                w[j][i] = 1;
        }
    }
    vis[0] = 1;
    ans[0] = 0;
    if(!dfs(0,1))
        printf("No Solution");
    return  0;
}

數獨挑戰

題目鏈接

題解


=======最後更新時間:2019.07.09=======

=======遇到好的題目會回來繼續更新======

===如果各位大牛牪犇路過發現問題歡迎反映  (「・ω・)「嘿===

附上另外一篇:BFS的理解和應用,歡迎前去圍觀~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章