深度優先搜索算法是一種用於遍歷或搜索樹或圖的算法
建立一顆搜索樹,沿着樹的深度遍歷樹的節點,儘可能深的搜索樹的分支。當節點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
洛谷題目鏈接
基礎的回溯,關鍵是對角線上的處理(如何區分左對角線和右對角線)。
處理方法:
對角線上左上的對角線爲定值,右上的對角線也爲定值,爲防止小於零所以用,一樣爲定值。
#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表示該點不能放(與其他位置的皇后排斥或初始狀態就不能放).0表示該點可以放皇后
用sta[]來存儲初始狀態,將’.'位 置爲1
dfs保存四個參數:當前行的狀態,從左上到右下對角線的狀態,從右上到左下對角線的狀態,當前爲第幾行
獲取當前哪一位可以放置皇后:將四者取並集(即將四者進行或運算).得到的狀態中爲0的就可以放置皇后.
爲了快速得到可以放置皇后的位置,對上一步得到的狀態進行取反.轉換成快速得到1的位置.
用樹狀數組中的lowbit()就可以得到從右向左的第一個1
將狀態中的1減掉,繼續找下一個1
更新"將是下一行的狀態",由於對角線是斜着影響的,所以左上到右下對角線的狀態需要右移一位,右上到左下對角線的狀態需要左移一位.
知道當前行的狀態全爲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,需要簡單地剪枝,首先如果當前搜的過程中答案已經大於最優解,很顯然可以直接。
然後就是因爲小貓的重量不同,那麼肯定是先爲最重的小貓安排車會是最優的選項,所以應該先把小貓按照重量從大到小排個序再開始爆搜。
#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)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意爲您解答!( •̀ ω •́ )✧