0x22.搜索 - 深度優先搜索

深度優先搜索算法是一種用於遍歷或搜索樹或圖的算法

建立一顆搜索樹,沿着樹的深度遍歷樹的節點,儘可能深的搜索樹的分支。當節點v的所在邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點爲止。如果還存在未被發現的節點,則選擇其中一個作爲源節點並重復以上過程,整個進程反覆進行直到所有節點都被訪問爲止。

一、DFS三個經典NPC問題

三個經典深搜入門級問題。
子集和問題,全排列問題,N皇后問題。

1.子集和問題

子集和問題的一個實例爲<S,c>。其中S={x1,x2,…,xn}是一個正整數的集合,c是一個正整數。子集和問題判定是否存在S的一個子集S1,使得S1中所有元素的和爲c。

跟0x03遞歸那一節的差不多回溯遞歸

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<unordered_map>
#define ls (p<<1)
#define rs (p<<1|1)
//#pragma GCC optimize (2)
//#pragma G++ optimize (2)//手動開 O2
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
using namespace std;
#undef mid
typedef long long ll;
typedef pair<int,int> PII;
const int N = 30007;
const ll mod = 1e9+7;
const ll INF = 1e15+7;
const double EPS = 1e-10;
const int base = 131;//13331
int n,m,sum;
int a[N];
bool vis[N],flag;
void calc(int x){
    if(flag||vis[x]||x>n+1)return ;
    if(sum == m){
        flag = 1;
        over(i,1,n){
            if(vis[i])printf("%d ",a[i]);
        }
        return;
    }
    calc(x+1);//不選
    sum += a[x];
    vis[x] = 1;
    calc(x+1);//選
    vis[x] = 0;//回溯
    sum -= a[x];
}
int main()
{
    scanf("%d%d",&n,&m);
    over(i,1,n)
    scanf("%d",&a[i]);
    calc(1);
    if(!flag)puts("NO");
    return 0;
}

2.全排列問題

給定一個由不同的小寫字母組成的字符串,輸出這個字符串的所有全排列。

這個懶得敲了,直接複製一個網上的代碼好了

#include <iostream>
#include <string>
#include <string.h>
#include <stdlib.h>
using namespace std;

char allprem[1000][7];
int count = 0;

void swap(char* str,int a,int b)
{
    char tmp = str[b];
    str[b] = str[a];
    str[a] = tmp;
}
void perm(char* str,int start,int end)
{
    if(start == end - 1)
    {
        strcpy(allprem[count],str);
        count++;
    }
    else
    {
        for(int k = start;k < end;k++)
        {
            swap(str,k,start);
            perm(str,start+1,end);
            swap(str,k,start);
        }
    }
}

int compare(const void * a,const void* b)
{
    return strcmp((char*)a,(char*)b);
}
int main()
{
    char inputStrr[7] = {0};
    while(cin >> inputStrr)
    {   
        count = 0;
        perm(inputStrr,0,strlen(inputStrr));
        
        qsort(allprem,count,sizeof(allprem[0]),compare);
        
        for(int i = 0;i < count;i++)
            cout << allprem[i] << endl;
    }
    return 0;
}


3.N皇后問題

luogu P1219 [USACO1.5]八皇后 Checker Challenge

洛谷題目鏈接
在這裏插入圖片描述
基礎的dfsdfs回溯,關鍵是對角線上的處理(如何區分左對角線和右對角線)。
處理方法:
對角線上左上的對角線i+ji+j爲定值,右上的對角線iji-j也爲定值,爲防止小於零所以用ij+ni-j+n,一樣爲定值。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<unordered_map>
#define ls (p<<1)
#define rs (p<<1|1)
#pragma GCC optimize (2)
#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;

const int N = 30007;
const ll mod = 1e9+7;
const ll INF = 1e15+7;
const double EPS = 1e-10;
const int base = 131;

int n,m;
int a[100];
int check[3][100];
int sum;
int tot;
void dfs(int now){
    if(now>n){
        tot++;
        if(tot > 3)return ;
        over(i,1,n)
        printf("%d ",a[i]);
        puts("");
        return ;
    }
    over(i,1,n){
        if(!check[0][i]&&!check[1][now+i]&&!check[2][now-i+n]){
            a[now] = i;
            check[0][i] = 1,check[1][i+now] = 1,check[2][now-i+n] = 1;
            dfs(now+1);
            check[0][i] = 0,check[1][i+now] = 0,check[2][now-i+n] = 0;
        }
    }
}
int main()
{
    scanf("%d",&n);
    dfs(1);
    printf("%d\n",tot);
    return 0;
}

luogu P1562 還是N皇后(狀態壓縮)

洛谷題目鏈接
在這裏插入圖片描述

這裏我所習慣的二進制還是右邊是第0位開始,這樣正好與實際情況相反,所以都要反着來。

  1. 逐行放置皇后,首先排除每行有多個皇后互相排斥的情況

  2. 用二進制表示狀態.1表示該點不能放(與其他位置的皇后排斥或初始狀態就不能放).0表示該點可以放皇后

  3. 用sta[]來存儲初始狀態,將’.'位 置爲1

  4. dfs保存四個參數:當前行的狀態,從左上到右下對角線的狀態,從右上到左下對角線的狀態,當前爲第幾行

  5. 獲取當前哪一位可以放置皇后:將四者取並集(即將四者進行或運算).得到的狀態中爲0的就可以放置皇后.

  6. 爲了快速得到可以放置皇后的位置,對上一步得到的狀態進行取反.轉換成快速得到1的位置.

  7. 用樹狀數組中的lowbit()就可以得到從右向左的第一個1

  8. 將狀態中的1減掉,繼續找下一個1

  9. 更新"將是下一行的狀態",由於對角線是斜着影響的,所以左上到右下對角線的狀態需要右移一位,右上到左下對角線的狀態需要左移一位.

  10. 知道當前行的狀態全爲1時,即每一行都有一個皇后時,ans++;

思路
還有就是涉及到位運算狀態壓縮我還是習慣於從第0位開始,所以所有的循環都要統一從0開始。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<unordered_map>
#define ls (p<<1)
#define rs (p<<1|1)
#pragma GCC optimize (2)
#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
#undef mid
typedef long long ll;
typedef pair<int,int> PII;

const int N = 100;
const ll mod = 1e9+7;
const ll INF = 1e15+7;
const double EPS = 1e-10;
const int base = 131;

int n,m;
char ch[N];
int state[N];
int ach,ans;

void dfs(int now,int l,int r,int line){
    if(now == ach){
        ans++;
        return ;
    }
    int pos = ach&(~(now|l|r|state[line]));//pos裏1的可以放置皇后
    int p;
    while(pos){//把pos裏每一個1都取出來
        p = lowbit(pos);
        pos -= p;
        dfs(now+p,(l+p)>>1,(r+p)<<1,line+1);
    }
}
int main()
{
    scanf("%d",&n);
    ach = (1<<n)-1;//從0到n-1全部放完
    for(int i = 0;i < n;++i){
        scanf("%s",ch);
        getchar();
        over(j,0,n-1)
            if(ch[j] == '.')
                state[i] |= (1<<(j));//第j位 置1
    }
    dfs(0,0,0,0);
    printf("%d\n",ans);
    return 0;
}

二、DFS習題

1.AcWing 165. 小貓爬山

在這裏插入圖片描述
直接搜索,因爲只有兩個狀態,一個是利用已有的纜車,一個是新增一輛纜車。就直接爆搜,把所有的情況都搜一遍,取最優解。注意回溯。
但是直接爆搜會T,需要簡單地剪枝,首先如果當前搜的過程中答案已經大於最優解,很顯然可以直接returnreturn
然後就是因爲小貓的重量不同,那麼肯定是先爲最重的小貓安排車會是最優的選項,所以應該先把小貓按照重量從大到小排個序再開始爆搜。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<unordered_map>
#define ls (p<<1)
#define rs (p<<1|1)
#pragma GCC optimize (2)
#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
#undef mid
typedef long long ll;
typedef pair<int,int> PII;

const int N = 100;
const ll mod = 1e9+7;
const ll INF = 1e15+7;
const double EPS = 1e-10;
const int base = 131;


int a[N];
int n,w;
int ans;
int cab[N];

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(cab[i] + a[now] <= w){
            cab[i] += a[now];
            dfs(now+1,cnt);
            cab[i] -= a[now];//回溯
        }
    }

    //租一輛新車
    cab[cnt+1] = a[now];
    dfs(now+1,cnt+1);
    cab[cnt+1] = 0;//回溯
}

int main()
{
    scanf("%d%d",&n,&w);
    over(i,1,n)
    scanf("%d",&a[i]);
    sort(a+1,a+1+n);
    reverse(a+1,a+1+n);
    ans = n;//最多用n輛車,最後取最小值所以要先最大化
    dfs(1,0);
    printf("%d\n",ans);
    return 0;
}

AcWing 166. 數獨

在這裏插入圖片描述
首先需要從當前能填合法數字最小的位置開始填數字
排除等效冗餘:任意一個狀態下,我們只需要找一個位置填數即可,而不是找所有的位置和可填的數字.
位運算:很明顯這裏面check判定很多,我們必須優化這個check,所以我們可以對於,每一行,每一列,每一個九宮格,都利用一個九位二進制數保存,當前還有哪些數字可以填寫.
lowbit:我們這道題目當前得需要用lowbit運算取出當前可以能填的數字.

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<vector>
#include<unordered_map>
#define ls (p<<1)
#define rs (p<<1|1)
#pragma GCC optimize (2)
#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)//返回的是一個二進制數,通過預處理的num數組可以把這個數再轉換回整數
using namespace std;
#undef mid
typedef long long ll;
typedef pair<int,int> PII;

const int N = 100;
const ll mod = 1e9+7;
const ll INF = 1e15+7;
const double EPS = 1e-10;
const int base = 131;

char str[N][N];
char s[N];
int n,m;
int num[1000],cnt[1000];
int hang[N],lie[N],ge[N],tot;

inline int getone(int x,int y){//得到是第幾個九宮格
    return ((x/3)*3)+(y/3);//因爲這裏是從0開始的0~8,如果是從1開始的要先-1
}

//1是能放置,0是不能放置

inline void calc(int x,int y,int z){//二進制修改第z位(從第0位開始的第z位)
    hang[x]^=1<<z;//行
    lie[y]^=1<<z;//列
    ge[getone(x,y)]^=1<<z;//九宮格
}

inline bool dfs(int now){
    if(now == 0)
        return 1;
    int tmp = 10,x,y;
    over(i,0,8)over(j,0,8){
        if(str[i][j] != '.')
            continue;
        int val = hang[i]&lie[j]&ge[getone(i,j)];
        if(!val)
        return 0;
        if(cnt[val]<tmp){//找到當前能夠合法填上的數字最小(cnt[i]最小,1最少)的位置
            tmp = cnt[val];
            x = i,y = j;
        }
    }
    int val = hang[x]&lie[y]&ge[getone(x,y)];
    for(;val;val-=lowbit(val)){//遍歷所有的1
        int z = num[lowbit(val)];
        str[x][y] = '1'+z;
        calc(x,y,z);//修改
        if(dfs(now-1))//下一位
            return 1;
        calc(x,y,z);//回溯,再改回來
        str[x][y] = '.';
    }
    return 0;
}

int main()
{
    for(int i = 0;i<1<<9;++i)
        for(int j = i;j;j -= lowbit(j))
            cnt[i]++;//對於每一個二進制數i,一共有幾個 1 這裏有幾個1就是代表數是幾(0~9)
    over(i,0,8)
    num[1<<i] = i;//lowbit返回的是一個二進制數,通過預處理的num數組可以把這個數再轉換回整數,既是將要填寫的數
    while(~scanf("%s",s)&&s[0]!='e'){
        over(i,0,8)over(j,0,8)
            str[i][j] = s[i*9+j];//還原成矩陣
        over(i,0,8)
        hang[i] = lie[i] = ge[i] = (1<<9)-1;//初始化爲全部都是1(全部都能取)
        tot = 0;
        over(i,0,8)over(j,0,8){
            if(str[i][j] != '.')
            calc(i,j,str[i][j]-'1');//這一位不能取,置爲 0 
            else tot++;//記錄一共要填幾個數
        }
        dfs(tot);
        over(i,0,8)over(j,0,8)
        s[i*9+j] = str[i][j];//還原爲一維數組輸出
        puts(s);
    }
    return 0;
}

注:如果您通過本文,有(qi)用(guai)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意爲您解答!( •̀ ω •́ )✧

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