0x20 搜索

0x21 樹與圖的遍歷

樹與圖的深度優先遍歷

深度優先遍歷,就是在每一個點\(x\)上面的的多條分支時,任意選擇一條邊走下去,執行遞歸,直到回溯到點x後再走其餘的邊node

int head[N];
bool v[N];
struct edge
{
    int v , next;
}e[N];

inline void dfs( int x )
{
    v[x] = 1;
    for( register int i = head[x] ; i ; i = e[i].next)
    {
        register int y = e[i].next;
        if( v[y] ) continue;
        dfs( y ) ;
    }
    return ;
}

樹的DFS序

通常來講,咱們在對樹的進行深度優先時,對於每一個節點,在剛進入遞歸時和回溯前各記錄一次該點的編號,最後會產生一個長度爲\(2N\)的序列,就成爲該樹的\(DFS\)c++

\(DFS\)序的特色時:每一個節點的\(x\)的編號在序列中剛好出現兩次。設這兩次出現的位置時\(L[x]\),\(R[x]\),那麼閉區間\([L[x],R[x]]\)就是以\(x\)爲根的子樹的\(DFS\)算法

inline void dfs( int x )
{
    a[ ++ tot ] = x; // a儲存的是DFS序
    v[ x ] = 1;
    for( register int i = head[x] ; i ; i = e[i].next )
    {
        register int y = e[i].v;
        if( v[y] ) continue;
        dfs( y );
    }
    a[ ++ tot ] = x;
    return ;
}

樹的深度

樹中各個節點的深度是一種自頂向下的統計信息數組

起初,咱們已知根節點深度是\(0\).若節點\(x\)的深度爲\(d[x]\),則它的節點\(y\)的深度就是\(d[y] = d[x] + 1\)框架

inline void dfs( int x )
{
    v[ x ] = 1;
    for( register int i = head[ x ] ; i ; i = e[i].next )
    {
        register int y = e[i].v;
        if( v[ y ] ) continue;
        d[ y ] = d[ x ] + 1; // d[]就是深度
        dfs( y );
    }
    return ;
}

樹的重心

對於一個節點\(x\),若是咱們把它從樹中刪除,呢麼原來的一顆樹可能會被分割成若干個樹。設\(max\_part(x)\)表示在刪除節點\(x\)後產生子樹中最大的一顆的大小。使\(max\_part(p)\)最下的\(p\)就是樹的重心函數

inline void dfs( int x )
{
    v[ x ] = 1 , size[ x ] = 1;//size 表示x的子樹大小 
    register int max_part = 0; // 記錄刪掉x後最大一顆子樹的大小 
    for( register int i = head[ x ] ; i ; i = e[i].next )
    {
        register int y = e[i].v;
        if( v[y] ) continue;
        dfs( y );
        size[x] += size[y];
    } 
    max_part = max ( max_part , n - size[x] );
    if( max_part < ans ) //全局變量ans記錄重心對應的max_part 
    { 
        ans = max_part;
        pos = x;//pos 重心 
    }
    return ;
}

圖的聯通塊劃分

若在一個無向圖中的一個子圖中任意兩個點之間都存在一條路徑(能夠相互到達),而且這個子圖是「極大的」(不能在擴展),則稱該子圖是原圖的一個聯通塊優化

以下代碼所示,cnt是聯通塊的個數,v記錄的是每個點屬於哪個聯通塊spa

inline void dfs( int x )
{
    v[ x ] = cnt;
    for( register int i = head[x] ; i ; i = e[i].next ) 
    {
        register int y = e[i].v;
        if( v[y] ) continue;
        dfs(y);
    }
    return ;
}

for( register int i = 1 ; i < = n ; i ++ )
{
    if( v[i] ) continue;
    cnt ++ ;
    dfs( i );
}

圖的廣度優先搜索遍歷

樹與圖的廣度優先遍歷是利用一個隊列來實現的設計

queue< int  > q;

inline void bfs()
{
    q.push( 1 ) , d[1] = 1;
    while( !q.empty() )
    {
        register int x = q.front(); q.pop();
        for( register int i = head[ x ] ; i ; i = e[i].next )
        {
            register int y = e[i].v;
            if( d[y] ) continue;
            d[y] = d[x] + 1;
            q.push(y);
        } 
    }
    return ;
}

上面的代碼中,咱們在廣度優先搜索中順便求了個樹的深度\(d\)code

拓撲排序

給定一張有向無環圖,若一個序列A知足圖中的任意一條邊(x,y)x都在y的前面呢麼序列A就是圖的拓撲排序

求拓撲序的過程很是簡單咱們只須要不斷將入度爲0的點加入序列中便可

  1. 創建空拓撲序列A
  2. 預處理出全部入度爲deg[i],起初把入度爲0的點入隊
  3. 取出對頭節點x,並把x放入序列A中
  4. 對於從x出發的每條邊(x,y),把deg[y]減1,若deg[y] = 0 ,把y加入隊列中
  5. 重複3,4直到隊列爲空,此時A即爲所求
inline void addedge( int x , int y )
{
    e[ ++ tot ].v = y , e[ tot ].next = head[x] , head[x] = tot;
    deg[x] ++;
} 

inline void topsort()
{
    queue< int > q;
    for( register int i = 1 ; i <= n ; i ++ )
    {
        if( !deg[i] ) q.push( i );
    } 
    while( !q.empty() )
    {
        register int x = q.front(); q.pop();
        a[ ++ cnt ] = x;
        for( register int i = head[x] ; i ; i = e[i].next )
        {
            register int y = e[i].v;
            if( -- deg[y] == 0 ) q.push( y );
        }
    }
    return ;
}

AcWing 164. 可達性統計

這道題的題意很簡單,可是若是直接裸的計算會超時,因此要用拓撲序

首先求拓撲序,由於拓撲序中的每個點都時由前面的點到的因此咱們反過來從最後一個點開始

假設咱們已經求得了\(x\)後面每個點的所能到達的點,呢麼咱們對全部以x爲起點的邊所到達的點所能到達的點取並集就是\(x\)所等到達的全部的點

而後若是們要儲存每一個點所到達的點,若是咱們用二維數組來存,會爆空間,因此爲了節約空間能夠用<bitset>來存

#include <bits/stdc++.h>
using namespace std;

const int N = 30010;
int n , m , head[N] , d[N]  , a[N] , tot , cnt ; 
bitset< N > f[N];

struct edge
{
    int v , next;
}e[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 1 ) + ( x << 3 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void addedge( int u , int v )
{
    e[ ++ tot ].v = v , e[ tot ].next = head[u] , head[u] = tot;
    d[ v ] ++;
}

inline void topsort()
{
    queue< int > q;
    for( register int i = 1 ; i <= n ; i ++ )
    {
        if( !d[i] ) q.push( i );
    }
    while( !q.empty() )
    {
        register int x = q.front(); q.pop();
        a[ ++ cnt ] = x;
        for( register int i = head[x] ; i ; i = e[i].next )
        {
            register int y = e[i].v;
            if( -- d[y] == 0 ) q.push( y );
        }
    }
    return ;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 ; i <= m ; i ++ )
    {
        register int a = read() , b = read();
        addedge( a , b );
    }
    
    topsort();
    
    for( register int i = cnt , j = a[i] ; i ; i -- , j = a[i] )
    {
        f[j][j] = 1;
        for( register int k = head[j] ; k ; k = e[k].next ) f[j] |= f[ e[k].v ];
    }
    
    for( register int i = 1 ; i <= n ; i ++ ) printf( "%d\n" , f[i].count() );
    return 0;
}

0x22 深度優先搜索

深度優先搜索算法\((Depth-First-Search)\)是一種用於遍歷或搜索樹或圖的算法

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

AcWing 165. 小貓登山

這道題時dfs最基礎的題目了

咱們只需設計搜索的狀態這道題就能夠輕易的寫出來

咱們設(x,y)是搜索的狀態即前x個小貓用了y個纜車

咱們要轉移的狀況只有兩種

  1. 小貓上前y輛纜車
  2. 小貓上y+1輛纜車(新開一輛)

因此咱們只要枚舉就好

而後就是如何優化算法

首先假如咱們已經獲得一個解pay,若此時的大於pay則不可能會更優,因此能夠本身而回溯

而後咱們把小貓從大到小排序能夠排除不少不多是結果的狀況

#include <bits/stdc++.h>
using namespace std;


const int N = 20;
int n , w , c[N] , f[N], pay = N;

inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    } 
    return x;
}

inline void dfs( int x , int y )
{
    if( x > n )
    {
        pay = min( pay , y );
        return ;
    }
    if( y > pay ) return ;
    
    for( register int i = 1 ; i <= y ; i ++ )
    {
        if( f[i] + c[x] > w ) continue;
        f[i] += c[x];
        dfs( x + 1 , y );
        f[i] -= c[x];
    }
    y ++;
    f[y] += c[x];
    dfs( x + 1 , y );
    f[y] = 0;
    return ;
}

inline bool cmp( int x , int y ) { return x > y; }


int main()
{
    n = read() , w = read();
    for( register int i = 1 ; i <= n ; i ++ ) c[i] = read();
    
    sort( c + 1 , c + 1 + n , cmp );
    dfs( 1 , 0 );
    
    cout << pay << endl;
    return 0;
}

AcWing 166. 數獨

這是一道經典的搜索題,不過是數據增強過的版本,全部直接搜索會T

必需要進行一些優化

首先咱們想本身玩數獨的時候是怎麼玩的

確定是首先填可能的結果最少的格子,在也是這道題優化的核心

如何快速的肯定每一個格子的狀況?

const int n = 9;
int row[n] , col[n] , cell[3][3];
// row[] 表示行 col表示列 cell[][] 表示格子

咱們用一個九位二進制數來表示某一行、某一列、或某一箇中能夠填入的數,其中1表示能夠填,0表示不能填

對於\((x,y)\)這個點咱們只需\(row[x] \bigcap col[y] \bigcap cell[\frac{x}{3}][\frac{y}{3}]\)就能夠知道這個點能夠填入數字的集合而後用lowbit()把每一位取出來便可

而在二進制中的交集就是$& $操做,因此取交集的函數就是

inline int get( int x , int y )
{
    return row[ x ] & col[ y ] & cell[ x / 3 ][ y / 3];
}

還有什麼優化呢,lowbit()的時間複雜度是\(O(log(n))\)咱們能夠經過預處理把一些操做變成\(O(1)\)

首先每次lowbit()獲得的並非最後一個必定位置而是一個二進制數,能夠用這個maps[]\(O(1)\)查詢最後一爲的具體位置

for( register int i = 0 ; i < n ; i ++ ) maps[ 1 << i ] = i;

其次對於每一個二進制數中有多少個\(1\)的查詢也是很慢的,能夠用這個ones[]\(O(1)\)查詢一個二進制數中有多少個\(1\)

for( register int i = 0 , s = 0 ; i < 1 << n ; i ++  , s = 0)
{
    for( register int j = i ; j ; j -= lowbit( j ) ) s ++;
    ones[ i ] = s;
}

剩下就是常規的\(DSF\)

#include <bits/stdc++.h>
#define lowbit( x ) ( x & -x )
using namespace std;


const int N = 100 , n = 9;
int maps[ 1 << n ] , ones[ 1 << n ] , row[n] , col[n] , cell[3][3];
char str[N];


inline void init() //初始化 
{
    for( register int i = 0 ; i < n ; i ++ ) row[i] = col[i] = ( 1 << n ) - 1 ;
    for( register int i = 0 ; i < 3 ; i ++ )
    {
        for( register 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];
}

inline bool dfs( int cnt )
{
    if( !cnt ) return 1; // 已經填滿 
    
    register int minv = 10 , x , y;
    for( register int i = 0 ; i < n ; i ++ )
    {
        for( register int j = 0 ; j < n ; j ++ )
        {
            if( str[ i * 9 + j ] != '.' ) continue;
            register int t = ones[ get( i , j ) ];
            if( t < minv ) // 找到可能狀況最少的格子 
            {
                minv  = t;
                x = i , y = j ;
            }
        }
    }
    
    for( register int i = get( x , y ) ; i ; i -= lowbit( i ) ) // 枚舉這個格子填那些數 
    {
        register int t = maps[ lowbit(i) ];
        row[x] -= 1 << t , col[y] -= 1 << t; // 打標記 
        cell[ x / 3 ][ y / 3 ] -= 1 << t;
        str[ x * 9 + y ] = t + '1';
        
        if( dfs(cnt - 1 ) ) return 1;
        
        row[x] += 1 << t , col[y] += 1 << t; // 刪除標記 
        cell[ x / 3 ][ y / 3 ] += 1 << t;
        str[ x * 9 + y ] = '.';
    }
    return 0;
}


int main()
{
    for( register int i = 0 ; i < n ; i ++ ) maps[ 1 << i ] = i;
    for( register int i = 0 , s = 0 ; i < 1 << n ; i ++  , s = 0)
    {
        for( register int j = i ; j ; j -= lowbit( j ) ) s ++;
        ones[ i ] = s; // i 這個數二進制中有多少個 1 
    }
    
    while( cin >> str , str[0] != 'e' )
    {
        init();
        
        register int cnt = 0;
        for( register int i = 0 , k = 0 ; i < n ; i ++ )
        {
            for( register int j = 0 ; j < n ; j ++ , k ++ )
            {
                if(str[k] == '.' ) { cnt ++ ; continue; } //記錄有多少個數字沒有填 
            
                register int t = str[k] - '1'; // 把已經填入的數字刪除 
                row[ i ] -= 1 << t;
                col[ j ] -= 1 << t;
                cell[ i / 3 ][ j / 3 ] -= 1 << t;
            }
        }
        
        dfs( cnt );
        
        cout << str << endl;
    }
        
    return 0;
}

0x23 剪枝

剪枝,就是減少搜索樹的規模、儘早的排除搜索樹中沒必要要的成分

  1. 優化搜索順序
    在一些問題中,搜索樹的各個層次、各個分支的順序是不固定的。不一樣的搜索順序會產生不一樣的搜索樹形態,其規模相差也很大。咱們能夠經過優先搜索更有可能出現結果的分支來提早找到答案
  2. 排除等效冗餘
    在搜索的過程當中,若是可以斷定搜索樹上當前節點的幾個分支是等效的,這咱們搜索其中一個分支便可
  3. 可行性剪枝
    在搜索的過程當中對當前的狀態進行檢查,若是不管如何都不可能走到邊界咱們就放棄搜索當前子樹,直接回溯
  4. 最優性剪枝
    在搜索過程當中假設咱們已經找到了某一個解,若是咱們目前的狀態比已知解更劣就放棄繼續搜索下去由於沒法比當前解更優呢麼後面狀況累加起來後必定比當前解更劣,因此直接回溯
  5. 記憶化
    能夠記錄每一個狀態的結果,在每次遍歷過程當中檢查當前狀態是否已經被訪問過,若果被訪問過直接返回以前搜索的結果

AcWing 167.木棒

這是一道經典的剪枝題

優化搜索順序

  1. 把木棍從大到小排序,優先嚐試比較長的木棍,越短的木棍適應能力越強

排除等效冗餘

  1. 限制加入木棍的順序必須是遞減的,由於假若有兩根木棍\(x,y(x<y)\),先加入\(x\)和先加入\(y\)是等效的
  2. 若是上一根木棍失敗且和當前木棍長度相同,這當前木棍必定失敗
  3. 如過當前木棍已經拼成一個完整的木棍,當後面拼接過程當中失敗則當前木棍不管怎麼拼都必定會失敗,由於在從新嘗試的過程當中會使用更多更小的木棍來拼成當前木棍,但更小的木棍的適用性更強,卻失敗了,因此用更長的木棍嘗試也必定會失敗
#include <bits/stdc++.h>
#pragma GCC optimize(3,"Ofast","inline")
#pragma GCC optimize(2)
using namespace std;


const int N = 100;
int n , m , a[N] , sum , cnt , len ;
bool v[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline bool cmp( int x , int  y ) { return x > y; }

inline bool dfs( int stick , int cur , int last)
{
    if ( stick == cnt ) return 1;
    if ( cur == len ) return dfs( stick + 1 , 0 , 1 );
    
    register int fail = 0;
    for (register int i = last; i <= n; i++) // 剪枝 2  
    {
        if( v[i] || cur + a[i] > len || fail == a[i] ) continue;
        // fail == a[i] 剪枝 3 
        v[i] = 1;
        if ( dfs( stick , cur + a[i] , i + 1 ) ) return 1;
        v[i] = 0 , fail = a[i];
        if ( cur == 0 || cur + a[i] == len ) break;
        // cur + a[i] = len 剪枝 4 
    }
    return 0; 
}

inline void work()
{ 
    sum = n = 0;
    for( register int i = 1 ; i <= m ; i ++ )
    {
        register int x = read();
        if( x > 50 ) continue;
        a[ ++ n ] = x , sum += x;
    }
    
    sort( a + 1 , a + 1 + n , cmp );
    //剪枝 1 
    
    for( len  = a[1] ; len <= sum ; len ++ )
    {
        if( sum % len ) continue;
        cnt = sum / len;
        memset( v , 0 , sizeof( v ) );
        if( dfs( 1 , 0 , 1 ) ) break;
    }
    printf( "%d\n" , len );
    return ;
}


int main()
{
    while(1)
    {
        m = read();
        if( m == 0 ) break;
        work();
    }
    return 0;
}

0x24 迭代加深

深度優先搜索(\(ID-DSF\))就是每次選擇一個分支,而後不斷的一直搜索下去,直到搜索邊界在回溯,這種算法有必定的缺陷

好比下面這張圖,我要紅色點走到另外一個紅色點

若是用普通的\(DFS\)前面的不少狀態都是無用的,由於子樹太深了

而且每到一個節點我都要儲存不少的東西\(BFS\)很很差存

這是就要用到迭代加深了

AcWing 170. 加成序列

這道題就是一個迭代加深搜索的模板題

爲何是迭代加深搜索呢?

分析題目給的性質

若是使用\(DFS\),你須要搜索不少層,而且第一個找到的解不必定最有解

若是使用\(BFS\),你須要在隊列中儲存\(M\)個長度爲\(n\)的數組(\(M\)是隊列長度),不只儲存很是麻煩而且還有可能會爆棧

因此經過迭代加深性質就能很好的解決這個問題

限制序列的長度,不斷從已知的數中找兩個相加,到邊界時判斷一下,比較常規

優化搜索順序

  1. 爲了可以儘早的達到\(n\),從大到小枚舉\(i\)\(j\)

排除等效冗餘

  1. 由於\(i\)\(j\)\(j\)\(i\)是等效的因此保證\(j \le i\)
  2. 不一樣的\(i\),\(j\)可能出現\(a[i]+a[j]\)相同的狀況,對相加結果進行判重
#include <bits/stdc++.h>
using namespace std;


const int N = 105;
int n , m , a[N];
bitset< N > vis;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' ) 
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline bool ID_dfs( int k )
{
    if( k > m ) return a[m] == n;
    
    vis.reset();
    for( register int i = k - 1 ; i > 0 ; i -- )
    {
        for( register int j = k - 1 ; j > 0 ; j -- )
        {
            register int cur = a[i] + a[j];
            if( cur > n || vis[ cur ] || cur < a[ k - 1 ]) continue;
            a[k] = cur;
            vis[ cur ] = 1;
            if( ID_dfs( k + 1 ) ) return 1;
        }
    }
    return 0;
}

inline void work()
{
    for( m = 1 ; m <= n ; m ++ )
    {
        if( ID_dfs( 2 ) ) break;
    }
    for( register int i = 1 ; i <= m ; i ++ ) printf( "%d " , a[i] );
    puts("");
    return ;
}


int main()
{
    a[1] = 1 , a[2] = 2;
    for( n = read() ; n ; n = read() ) work();
    return 0;
}

雙向搜索

除了迭代加深外,雙向搜索也能夠大大減小在深沉子樹上浪費時間

在一些問題中有明確的初始狀態和末狀態,呢麼就能夠採用雙向搜索

從初狀態和末狀態出發各搜索一半,產生兩顆深度減半的搜索樹,在中間交匯組合成最終答案

AcWing 171.送禮物

這到題顯然是一個\(DP\),可是因爲它數字的範圍很是大作\(DP\)確定會\(T\)

因此這道題的正解就是\(DFS\)暴力枚舉全部可能在判斷

可是\(n\le 46\)因此搜索的複雜度是\(O(2^{46})\)依然會\(T\)

因此仍是要想辦法優化,這裏用了到了雙向搜索的思想

咱們將\(a[1\cdots n]\),分紅\(a[1\cdots mid]\)\(a[mid+1\cdots n]\)兩個序列

首先如今第一個序列中跑一次\(DFS\)求出因此能夠產生的合法狀況,去重,排序

而後在第二個序列中再跑一次\(DFS\),求出的每個解\(x\)就在第一序列產生的結構中二分一個\(y\)知足\(max(x),x\in\{ x | x + y \le W \}\),更新答案

優化

  1. 優化搜索順序,從大到小搜索,很常規
  2. 咱們發現第二次\(DFS\)中會屢次二分,因此咱們能夠適當的減小第二個序列長度,來平衡複雜度。換句話來講就是適當的減小二分的次數,根據實測\(mid=\frac{n}{2}+2\)效果最好
#include <bits/stdc++.h>
using namespace std;


const int N = 50;
int w , n ,tot , a[N] , mid , ans , m ;
vector < int > s;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void dfs_1( int x , long long sum )
{
    s.push_back( sum );
    if( x > mid ) return ;
    if( sum + a[x] <= w ) dfs_1( x + 1 , sum + a[x] );
    dfs_1( x + 1 , sum );
    return ;
}

inline void dfs_2( int x , long long sum )
{
    register auto t = lower_bound( s.begin() , s.end() , w - sum , greater<int>() );
    if( sum + *t <= w ) ans = max( ans , I(sum) + *t );
    if( x > n ) return ;
    if( sum + a[x] <= w ) dfs_2( x + 1 , sum + a[x] );
    dfs_2( x + 1 , sum );
    return ;
}


int main()
{
    w = read() , m = read() ; 
    for( register int i = 1 ; i <= m ; i ++ )
    {
        register int x = read();
        if( x > w ) continue;
        a[ ++ n ] = x;
    }

    mid = n >> 1 + 2;
    sort( a + 1 , a + 1 + n , greater<int>() );
    dfs_1( 1 , 0 );
    sort( s.begin() , s.end() );
    unique( s.begin() , s.end() );
    dfs_2( mid + 1 , 0);
    cout << ans << endl;
    return 0;
}

0x25 廣度優先搜索

\(BFS\) 全稱是 \(Breadth First Search\) ,中文名是寬度優先搜索,也叫廣度優先搜索。

是圖上最基礎、最重要的搜索算法之一。

所謂寬度優先。就是每次都嘗試訪問同一層的節點。 若是同一層都訪問完了,再訪問下一層。

這樣作的結果是,\(BFS\) 算法找到的路徑是從起點開始的 最短 合法路徑。換言之,這條路所包含的邊數最小。

\(BFS\) 結束時,每一個節點都是經過從起點到該點的最短路徑訪問的。

算法過程能夠看作是圖上火苗傳播的過程:最開始只有起點着火了,在每一時刻,有火的節點都向它相鄰的全部節點傳播火苗。

AcWing 172. 立體推箱子

這到題是寬搜中比較有難度的一道

這道題中不變的是圖,變化的是物體的狀態,因此本題的難點就在於如何設計狀態

咱們能夠用一個三元組\((x,y,lie)\)來表明一個狀態(搜索樹上的一個節點)

\(lie=0\)時,物體立在\((x,y)\)

\(lie=1\)時,物體橫向躺着,而且左半部分在\((x,y)\)

\(lie=2\)時,物體縱向躺着,而且上半部分在\((x,y)\)

而且用數組\(d[x][y][lie]\)表述從其實狀態到每一個狀態所須要的最短步數

設計好狀態就能夠開始搜索了

#include <bits/stdc++.h>
using namespace std;


const int N = 510;
const int dx[4] = { 0 , 0 , 1 , -1 } , dy[4] = { 1 , -1 , 0 , 0 };
const int next_x[3][4] = { { 0 , 0 , -2 , 1 } , { 0 , 0 , -1 , 1 } , { 0 , 0 , -1 , 2 } }; 
const int next_y[3][4] = { { -2 , 1 , 0 , 0 } , { -1 , 2 , 0 , 0 } , { -1 , 1 , 0 , 0 } }; 
const int next_lie[3][4] = { { 1 , 1 , 2 ,  2 } , { 0 , 0 , 1 , 1 } , { 2 , 2 , 0 , 0 } }; 
int n , m , d[N][N][3] , ans;
char s[N][N]; 
struct rec{ int x , y , lie; } st , ed ; //狀態
queue< rec > q;


inline bool valid( int x , int y ) { return x >= 1 && x <= n && y >= 1 && y <= m; }

bool operator == (rec a ,rec b ){ return a.x == b.y && a.y == b.y && a.lie == b.lie ;}

inline void pares_st_ed()
{
    for( register int i = 1 ; i <= n ; i ++ )
    {
        for( register int j = 1 ; j <= m ; j ++ )
        {
            if( s[i][j] == 'O') ed.x = i , ed.y = j , ed.lie = 0, s[i][j] = '.';
            else if( s[i][j] == 'X' )
            {
                for( int k = 0 ; k < 4 ; k ++ )
                {
                    register int x = i + dx[k] , y = j + dy[k];
                    if( valid( x , y ) && s[x][y] == 'X' )
                    {
                        st.x = min( i , x ) , st.y = min( j , y ) , st.lie = k < 2 ? 1 : 2;
                        s[i][j] = s[x][y] = '.';
                        break;
                    }
                }
            }
            if( s[i][j] == 'X' ) st.x = i , st.y = j , st.lie = 0;
        } 
    }
}

inline bool check( rec next )
{
    if( !valid( next.x , next.y ) ) return 0;
    if( s[next.x][next.y] == '#' ) return 0;
    if( next.lie == 0 && s[next.x][next.y] != '.' ) return 0;
    if( next.lie == 1 && s[next.x][next.y] == '#' ) return 0;
    if( next.lie == 2 && s[next.x][next.y] == '#' ) return 0;
    return 1; 
} 

int bfs() {
    for( register int i = 1 ; i <= n ; i ++ )
    {
         for( register int j = 1 ; j <= m ; j ++ )
         {
            for( register int k = 1 ; k <= n ; k ++ ) d[i][j][k] = -1;
         }
    }
    
    while( q.size() ) q.pop();
    d[st.x][st.y][st.lie] = 0 ; 
    q.push( st );
    rec now , next;
    
    while( q.size() )
    {
        now = q.front() , q.pop();
        for( int i = 0 ; i < 4; i ++ )
        {
            next.x = now.x + next_x[now.lie][i] , next.y = now.y + next_y[now.lie][i] , next.lie = next_lie[now.lie][i];
            if (!check(next)) continue;
            
            if (d[next.x][next.y][next.lie] == -1) 
            {  
                d[next.x][next.y][next.lie] = d[now.x][now.y][now.lie]+1;
                q.push(next);
                if (next.x == ed.x && next.y == ed.y && next.lie == ed.lie) return d[next.x][next.y][next.lie];  // 到達目標
            }
        }
    }
    return -1; 
}


int main()
{
    while( 1 )
    {
        cin >> n >> m;
        if( !n && !m ) break;
        
        for( register int i = 1 ; i <= n ; i ++ ) scanf( "%s" , s[i] + 1 );
        pares_st_ed();
        ans = bfs();
        if( ans == -1 ) puts("Impossible");
        else cout << ans << endl;
    }
    return 0;
}

在上述的代碼中使用了\(next\_x,next\_y,next\_lie\)這三個數組來表示向四個方向移動的變化狀況時寬搜中經常使用的一中技巧,避免了大量使用\(if\)語句容易形成混亂的狀況

Luogu P3456 GRZ-Ridges and Valleys

這道題時一個寬搜的經典題,若是用\(DFS\)會爆棧

看代碼就能夠理解

#include<bits/stdc++.h>
using namespace std;


const int N = 1005;
const int dx[8] = { -1 , -1 , -1 , 0 , 0 , 1 , 1 , 1 } , dy[8] = { -1 , 0 , 1 , -1 , 1 , -1 , 0 , 1 };
//向 8 個方向擴展
int n , maps[N][N] , valley , peak;
bool vis[N][N] , v , p;
struct node
{
    int x , y;
} _ , cur;// 儲存搜索狀態
queue< node > q;


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
} 

inline node make_node( int x , int y ) { _.x = x , _.y = y; return _; }


inline int bfs()
{
    register int ux , uy;
    while( !q.empty() )
    {
        cur = q.front() , q.pop();
        for( register int i = 0 ; i <= 7 ; i ++ )
        {
            ux = cur.x + dx[i] , uy = cur.y + dy[i];
            if( ux < 1 || ux > n || uy < 1 || uy > n ) continue;
            //判斷是否躍出邊界  
            if( maps[ux][uy] == maps[ cur.x ][ cur.y ] && !vis[ux][uy] )
            {//若是高度相同,打標記繼續搜索
                vis[ux][uy] = 1;
                q.push( make_node( ux , uy ) );
            }
            else//判斷當前聯通塊是 山峯 或 山谷
            {
                if( maps[ux][uy] > maps[cur.x][cur.y] ) p = 0;
                if( maps[ux][uy] < maps[cur.x][cur.y] ) v = 0;
            }
        }
    }
}    


int main()
{
    n = read() , v = 1;
    for( register int i = 1 ; i <= n ; i ++ )
    {
        for( register int j = 1 ; j <= n ; j ++ )
        {
            maps[i][j] = read();
            if( maps[i][j] != maps[1][1] ) v = 0; 
        }
    }
    
    if( v ) puts("1 1") , exit(0);//特殊狀況判斷
    
    for( register int i = 1 ; i <= n ; i ++ )
    {
        for( register int j = 1 ; j <= n ; j ++ )
        {
            if( vis[i][j] ) continue;//判斷當前點是不是被搜索的聯通塊
            v = p = vis[i][j] = 1;
            q.push( make_node( i , j ) );
            bfs();
            peak += p , valley += v;
        }
    }
    
    cout << peak << ' ' << valley << endl;
    return 0;
}

0x26廣搜變形

雙端隊列\(BFS\)

雙端隊列 \(BFS\) 又稱 \(0-1 BFS\)

適用範圍

在一張圖中,若是一張圖中,有些邊有邊權,有些邊沒有邊權,若是要搜索這個圖,就要用雙端隊列\(BFS\)

具體實現

在搜索過程當中,若是遇到的沒有邊權的邊就加入隊頭,若是有邊權就加入隊尾

AcWing 175. 電路維修

能夠把這張方格圖,抽象成點,而後把圖中有的邊當成邊權爲\(1\),把沒有的邊看成沒有邊權的邊

而後作雙端隊列\(BFS\)就好

#include <bits/stdc++.h>
#define PII pair< int , int >
using namespace std;


const int N = 510 , INF = 0x7f7f7f7f;
const int dx[4] = { -1 , -1 , 1 , 1 } , dy[4] = { -1 , 1 , 1 , -1 };
const int ix[4] = { -1 , -1 , 0 , 0 } , iy[4] = { -1 , 0 , 0 , -1 };
int n , m , T , t , d[N][N];
bool vis[N][N];
char g[N][N] , cs[] = "\\/\\/";


inline int bfs()
{
    deque< PII > q;
    memset( vis , 0 , sizeof( vis ) );
    memset( d , INF , sizeof( d ) );
    d[0][0] = 0;
    q.push_back( { 0 , 0 } );
    
    while( !q.empty() )
    {
        auto cur = q.front() ; q.pop_front();
        register int x = cur.first , y = cur.second;
        if( vis[x][y] ) continue;
        vis[x][y] = 1;
        
        for( register int i = 0 ; i < 4 ; i ++ )
        {
            register int a = x + dx[i] , b = y + dy[i];
            register int j = x + ix[i] , k = y + iy[i];
            if( a >= 0 && a <= n && b >= 0 && b <= m)
            { 
                register int w = 0;
                if( g[j][k] != cs[i] ) w = 1;
                if( d[a][b] > d[x][y] + w )
                {
                    d[a][b] = d[x][y] + w;
                    if( w ) q.push_back( { a , b } );
                    else q.push_front( { a , b } );
                }
            }
        }
    }
    if( d[n][m] == INF ) return -1;
    return d[n][m];
}

inline void work()
{
    cin >> n >> m;
    for( register int i = 0 ; i < n ; i ++ ) scanf( "%s" , g[i] );
    
    t = bfs();
    
    if( t == -1 ) puts("NO SOLUTION");
    else printf( "%d\n" , t );
    return ;
}


int main()
{
    cin >> T;
    while( T -- ) work();
    return 0;
}

優先隊列\(BFS\)

這裏就是利用優先隊列的性質,每次優先擴展最優的狀態

AcWing 176. 裝滿的油箱

這題要用到優先隊列,由於普通的DFS會超時的

首先咱們使用一個二元組\(\{city,fuel\}\)來表示一個狀態,每一個狀態的權值就是到達這個狀態所須要的權值

而後們把全部的狀態都放入一個堆中,而且按照權值從小到大排序

每次咱們去除堆頂的元素進行擴展

  1. 若是當前油箱尚未滿,就擴展\(\{city,fuel+1\}\)這個狀態
  2. 遍歷以當前邊爲起點的全部邊,若是當前油箱的油能夠到達下一個城市,就擴展\(\{v , fuel -d[city][v\}\)這個狀態

因此當咱們第一次從對頭取出終點,就是最優解

#include <bits/stdc++.h>
#define F first
#define S second
using namespace std;


const int N = 1005 , C  = 105 , INF = 0x7f7f7f7f;
int n , m , T , c , st , ed , tot , a[N] , head[N] , dist[N][C];
bool vis[N][C];
priority_queue< pair< int , pair< int , int > > > q;
vector< pair< int , int > > e[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}



inline void work()
{
    c = read() , st = read() , ed = read();
    while( !q.empty() ) q.pop();
    memset( vis , 0 , sizeof( vis ) );
    memset( dist , INF , sizeof( dist ) );
    
    dist[ st ][0] = 0;
    q.push( make_pair( 0 , make_pair( st , 0 ) ) );
    
    while( !q.empty() )
    {
        register int city = q.top().S.F , fuel = q.top().S.S;
        q.pop();
        if( city == ed )
        {
            cout << dist[city][fuel] << endl;
            return ;
        }
        
        if( vis[city][fuel] ) continue;
        vis[city][fuel] = 1;
        if( fuel < c && dist[city][fuel + 1 ] > dist[city][fuel] + a[city] )
        {
            dist[city][ fuel + 1 ] = dist[city][fuel] + a[city];
            q.push(make_pair( - dist[city][fuel] - a[city], make_pair( city , fuel + 1 ) ) );
        }
        for( auto it : e[city] )
        {
            register int y = it.F , z = it.S;
            if( z <= fuel && dist[y][ fuel - z ] > dist[city][fuel] )
            {
                dist[y][ fuel - z ] = dist[city][fuel];
                q.push(make_pair( - dist[city][fuel] , make_pair( y , fuel - z ) ) );
            }
        }
    }
    puts("impossible");
}


int main()
{
    n = read() , m = read();
    for( register int i = 0 ; i < n ; i ++ ) a[i] = read();
    for( register int i = 1 ; i <= m ; i ++ )
    {
        register int u = read() , v = read() , w = read();
        e[u].push_back( make_pair( v , w ) );
        e[v].push_back( make_pair( u , w ) );
    }
    
    T = read();
    while( T -- ) work();
    
    return 0;
}

雙向\(BFS\)

雙向BFS的思想和0x24中雙向搜索是相同的,由於BFS是逐層搜索,因此會更好理解,同時算法實現也很簡單

從起始狀態,目標狀態分別開始,兩邊輪流進行,每次各擴展一層。當兩邊各自有一個狀態在記錄數組中發生重複時,就說明搜索過程當中相遇,能夠合併各自出發點到當前的最少步數

//開始結點 和 目標結點 入隊列 q
//標記開始結點爲 1
//標記目標結點爲 2
while( !q.empty() )
{
    //從 q.front() 擴展出新的s個結點
    //若是 新擴展出的結點已經被其餘數字標記過
    //那麼 表示搜索的兩端碰撞
    //那麼 循環結束
    
    //若是 新的s個結點是從開始結點擴展來的
    //那麼 將這個s個結點標記爲1 而且入隊q 

    //若是 新的s個結點是從目標結點擴展來的
    //那麼 將這個s個結點標記爲2 而且入隊q
}

0x27 A*

注:本小結在敘述過程當中使用參照了\(cdcq\)\(thu\)\(ppt\),因此一些概念與咱們常規的定義略有衝突

在以前的優先隊列\(BFS\)中,咱們經過記錄從起始狀態到當前狀態的權值\(W\),而且按照\(W\)排序,這樣能夠減小許多沒必要要的搜索

這其實就是一種貪心的思想,若是遇到當前的權值比較小,但後面的權值很是大,此時在用這種套路就會增長不少沒必要要的搜索

因此也就有了啓發式搜索\(A\),首先咱們要定義一些符號方便理解

s//初始狀態
t//目標狀態
n//當前狀態
g*[n] //從 s 到 n 的最小代價
h*[n] //從 n 到 t 的最小代價
f*[n] = h*[n] + g*[n]//從 s 到 t 的最小代價

對於每一個狀態,咱們按照他的\(f[n]\)排序,每次取出最優解,擴展狀態,直到第一次擴展到\(t\),結束循環

雖然\(A\)算法保證必定能夠最早找到最優解,但多數時候會由於求\(h^*[n]\),會耗費很大的代價,致使時間複雜度變大

因此就有了另外一種算法最佳圖搜索算法\(A^*\),仍是咱們要定義一些符號

g[n] // g*[n] 的估計值 ,可是因爲咱們已經訪問到當前狀態因此g[n] == g*[n] 
h[n] // h*[n] 的估計值
f[n] = h[n] + g[n] // f*[n] 的估計值 稱爲估價函數

只要保證\(h[n] \le h^*[n]\),剩餘不變\(A\)算法就變成了\(A^*\)算法

能夠簡單的敘述下正確性,由於\(h[n] \le h^*[n]\),即便估計函數不太準確,致使路徑上的非最有狀態被提早擴展

可是因爲\(g[n]\)不斷累加,\(f[n]\)會不段的逼近\(f^*[n]\),因此最早擴展到狀態時必定仍是最優解,由於\(h[t]==0\)

另外若是\(h[n]=0\)的話,\(A^*\)算法就變成了優先隊列\(BFS\),因此優先隊列\(BFS\)就是估價函數不夠優秀的\(A^*\)算法

因此如何設計一個優秀的估價函數就是\(A^*\)算法的精髓

AcWing 178. 第K短路

據說由於數據比較水,因此能夠\(dijkstra\)\(k\)次彈出也能夠過

\(A^*\)的題都沒什麼好說的,只要知道怎麼設計估價函數其餘就是模板了

咱們看估價函數的定義式\(h[x]=f[x]+g[x]\)

咱們發現\(g[x]\)是關鍵,\(g[x]\)的定義就是從當前狀態的步數到目標狀態的可能步數,且必須保證\(g[x]\le g^*[x]\)

不難想到求個最短路就行了,不過要求的是多源單匯最短路,且圖是個有向圖,用\(floyed\)也是不合適的

因此咱們能夠在反向圖上跑從T出發的單源多匯最短路的值做爲\(g[x]\)便可

#include <bits/stdc++.h>
#define PII pair< int , int >
#define IPII pair< int , PII > 
#define F first
#define S second
using namespace std;


const int N = 1005 , INF = 0x7f7f7f7f ; 
int n , m , vis[N] , g[N] , st , ed , k , ans;
vector< PII > from[N] , to[N];


inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
}

inline void addedge( int u , int v , int w )
{
    to[u].push_back( { v , w } );
    from[v].push_back( { u ,w } );
}

inline void dijkstra()
{
    priority_queue< PII , vector < PII > , greater< PII > > q;
    memset( g , INF , sizeof( g ) );
    g[ed] = 0;
    q.push( { g[ed] , ed } );
    int u , v , dist , w ;
    while( !q.empty() )
    {
        u = q.top().S , dist = q.top().F , q.pop();
        if( vis[u] ) continue;
        for( auto it : from[u] )
        {
            v = it.F , w = it.S;
            if( g[v] <= g[u] + w ) continue;
            g[v] = g[u] + w;
            q.push( { g[v] , v } );
        }
    }
    return ;
}

inline void A_star()
{
    priority_queue< IPII , vector< IPII > , greater< IPII > > q;
    memset( vis , 0 , sizeof( vis ) );
    q.push( { g[st] , { st , 0 } } );
    int u , v , w , dist;
    while( !q.empty() )
    {
        u = q.top().S.F , dist = q.top().S.S , q.pop();

        if( vis[u] >= k ) continue;
        vis[u] ++;

        if( u == ed && vis[u] == k ) printf( "%d\n" , dist ) , exit(0);
        
        for( auto it : to[u] )
        {
            v = it.F , w = it.S;
            if( vis[v] >= k ) continue;
            q.push( { dist + w + g[v] , { v , dist + w } } );
        }
    }
    return ;
}


int main()
{
    n = read() , m = read();
    for( register int i = 1 ; i <= m ; i ++ )
    {
        register int u = read() , v = read() , w = read();
        addedge( u , v , w );
    }
    st = read() , ed = read() , k = read();
    if( st == ed ) k ++;

    dijkstra();

    A_star();

    puts( "-1" );
    return 0;
}

0x28 IDA*

\(cdcq\):\(ID\)仍是那個\(ID\),\(A^*\)仍是那個\(A^*\)

首先咱們設計一個估價函數,而後在\(ID-DFS\)的框架下進行搜索

若是當前深度+將來估計深度 > 深度的限制 當即回溯

這就是\(IDA^*\)的核心,換言之\(IDA^*\)就是迭代加深的\(A^*\)

\(IDA^*\)算法的實現流程基本和\(ID - DFS\)相同

只須要咋搜索每次執行前寫上這句便可

if( dep + f() > max_dep ) return ;

因此\(IDA^*\)\(A^*\)共同精髓都是設計估價函數

而且要保證\(f(x)\le f^*(x)\) ,證實以下

紅色點是起始狀態,綠色點是當前狀態,紫色爲目標,藍色線爲咱們迭代到的最大權值

咱們如今要估計綠色到紫色點的權值,若是我麼的估計值小於實際值,則已消耗的權值加估計值就必定小於最大權值這能夠繼續搜索

若是不能保證估計權值小於實際權值,則可能會出現已消耗的權值加估計值大於最大權值,此時就不會繼續搜索綠色點的子樹,也就不可能的到達紫色點

因此不保證估計值小於實際值就不能保證正確性

AcWing 180. 排書

這道題就是經典的\(IDA^*\)

因爲n比較小,且最多搜索5層,因此能夠直接用一個數組來存下每一層 的狀態

而後就是設計估價函數

咱們能夠每次修改一個區間

對於任意一種狀態下若是$p[i+1] \neq p[i]+1 \(則\)i$和i+1是必定要調開的,咱們把這種狀況稱做一個錯誤狀態

咱們統計一下錯誤的狀態爲\(cnt\)

咱們的每一次操做最多能夠改變3個錯誤狀態,因此最理想的狀態下就是$次能夠把整個序列調整成目標序列

因此就獲得了一種估價函數\(f() = \left \lceil \frac{cnt}{3} \right \rceil\)

#include <bits/stdc++.h>
using namespace std;


const int N = 20;
int T , n , q[N] , cur[5][N] , max_dep , ans;
bool flag;

inline int read()
{
    register int x = 0;
    register char ch = getchar();
    while( ch < '0' || ch > '9' ) ch = getchar();
    while( ch >= '0' && ch <= '9' )
    {
        x = ( x << 3 ) + ( x << 1 ) + ch - '0';
        ch = getchar();
    }
    return x;
} 

inline int f()
{
    register int cnt = 0;
    for( register int i = 1 ; i < n ; i ++ )
    {
        if( q[ i + 1 ] != q[i] + 1 ) cnt ++ ;
    }
    return (cnt + 2 ) / 3;
}
inline bool check()
{
    for( register int i = 1 ; i <= n ; i ++ ) 
    {
        if( q[i] == i ) continue;
        return 0;
    }
    return 1;
}
inline void ida_star( int dep )
{
    if( dep + f() > max_dep || flag ) return ;
    if( check() )
    {
        ans = dep , flag = 1;
        return ;
    }
    
    for( register int l = 1 ; l <= n ; l ++ )
    {
        for( register int r = l ; r <= n ; r ++ )
        {
            for( register int k = k + 1 ; k <= n ; k ++ )
            {
                memcpy( cur[ dep ] , q , sizeof( q ) );
                register int x , y;
                for( x = r + 1 , y = l ; x <= k ; x ++ , y ++ ) q[y] = cur[dep][x];
                for( x = l ; x <= r ; x ++ , y ++ ) q[y] = cur[dep][x];
                ida_star( dep + 1 );
                if( flag ) return ;
                memcpy( q , cur[dep] , sizeof( q ) );
            }
        }
    }
    
    return ;
}

inline void work()
{
    n = read() , flag = 0;
    for( register int i = 1 ; i <= n ; i ++ ) q[i] = read();
    
    for( max_dep = 1 ; max_dep <= 4 , !flag ; max_dep ++ ) ida_star(0);
    if( flag ) printf( "%d\n" , ans );
    else puts("5 or more");
    return ;
}

int main()
{
    T = read();
    while( T -- ) work();
    return 0;
}
相關文章
相關標籤/搜索