1. 程式人生 > 其它 >AcWing 【演算法提高課】筆記02——搜尋

AcWing 【演算法提高課】筆記02——搜尋

搜尋進階

22.4.14

(PS:還有 字串變換 A*兩題 生日蛋糕 迴轉遊戲 沒做)

感覺暫時用不上

BFS

1. Flood Fill

線上性時間複雜度內,找到某個點所在的連通塊

思路

統計連通塊個數(多個連通塊):逮著一個就開搜

連通性問題(能走多遠,迷宮性問題,一個連通塊);起點開始搜

池塘計數

普通八連通

城堡問題

考察讀入理解

int dx[] = {0, -1, 0, 1}, dy[] = {-1, 0, 1, 0}; //按照西北東南的順序

int bfs (int x, int y) {
    int area = 0;
    q.push ({x, y});
    area ++; //別忘了算自身
    vis[x][y] = true;

    while (!q.empty()) {
        auto tt = q.front();
        q.pop();

        for (int i = 0; i < 4; i ++) {
            int xx = tt.first + dx[i], yy = tt.second + dy[i];
            if (!Range (xx, yy) || vis[xx][yy])
                continue;
            if (a[tt.first][tt.second] >> i & 1) //二進位制表示四周的情況
                continue;
            q.push ({xx, yy});
            area ++;
            vis[xx][yy] = true;
        }
    }
    return area;

}

山峰和山谷

判斷聯通塊型別,多加兩個變數來判斷

核心程式碼

dfs()函式內部:

if (a[xx][yy] != a[tt.first][tt.second]) {
	if (a[xx][yy] > a[tt.first][tt.second])
        hh = true; //有比他高的,所以一定不是山峰
    else
        ll = true; //有比他矮的,所以一定不是山谷
}

//要注意vis出現在這裡是因為,不同高度的格子是可以重複遍歷的,相同的才要判重
else if (!vis[xx][yy]){
    q.push ({xx, yy});
    vis[xx][yy] = true;
}

main()函式內部:

if (!vis[i][j]) {
    bool hh = false, ll = false;  //hh代表有無比他高的,ll代表有無比他矮的
    bfs (i, j, hh, ll);
    if (!hh)
        cnt1 ++;  //沒有比他高的,是山峰
    if (!ll)
        cnt2 ++;  //沒有比他矮的,是山谷
}

2. 最短路模型

迷宮問題

多加一個:記錄該點是從哪個點走過來的

注意是從終點開始的BFS(反向搜的話輸出的路徑就是正向的)

void bfs (int x, int y) {
    q.push ({x, y});
    memset (ans, -1, sizeof ans);
    ans[x][y] = {0, 0};

    while (!q.empty()) {
        auto tt = q.front();
        q.pop();

        for (int i = 0; i < 4; i ++) {
            int xx = tt.first + dx[i], yy = tt.second + dy[i];
            if (!Range(xx, yy) || a[xx][yy])
                continue;
            if (ans[xx][yy].first != -1)  //已經被更新過了,必然不是最短
                continue; 

            q.push ({xx, yy});
            ans[xx][yy] = tt;

        }
    }
}

武士風度的牛

Code

抓住那頭牛

Code

這倆都是同一型別的簡單題

3. 多源BFS

只更新一次。反著來,通過1更新0

Code

4. 最小步數模型

稍顯煩人的模擬

大佬的Code

5. 雙端佇列廣搜

Code

無向圖,邊權為0 / 1 (0表示連通,1表示不連通),求起點到終點的最短路徑

(經典01問題)雙端佇列廣搜:邊權為1加到隊尾,邊權為0插到隊頭

一些性質:

當起點和終點的奇偶性不一樣時(到達不了),NO SOLUTION

搞清楚格點,和格子下標

實現:類dijkstra + deque維護

6. 雙向廣搜

龐大空間

每次選擇當前隊列當中元素數量較少的進行拓展

7. A*

useless 主要是我不會。。先放一放

DFS

0. 判斷是否需要回溯

若把圖當成固定的,那麼不需要回溯,只走一次(把點當作狀態)

若考慮圖變換,需要回溯,恢復狀態(把棋盤當作狀態)

1. 剪枝

1. 優化搜尋順序

優先搜尋分支少的節點

2. 排除等效冗餘

不要搜尋重複狀態

3. 可行性剪枝

不合法就退出

4. 最優性剪枝

已達最優狀態

5. 記憶化搜尋(DP)

例題

小貓爬山

貓貓!

填滿舊車,開新車

數獨

此題有點噁心

位運算優化:用一個9位01串來表示,再把行 列 九宮格 的狀態與起來,該位上為1,就代表可以放這個數字

木棍

讓人絕望的剪汁兒

  1. 列舉sum的約數(保證能被整除)
  2. 優化搜尋順序:先列舉長的木棍
  3. 排除等效冗餘:
    1. 按照組合數的方式來列舉
    2. 與已經失敗的木棍長度相同所有 的一定也不行
    3. 如果某木棒放第一根木棍u導致當前這根木棒湊不成length,整個方案一定失敗
    4. 如果木棒的最後一根木棍 u 放在這裡導致後續方案失敗,則整個方案一定失敗

好的講解

2. 迭代加深

優美的演算法

適用:層數很深,答案很淺

定一個層數上限,搜出去了就減掉

逐步擴大範圍

層層擴大,按層搜尋

剪枝

優化搜尋順序:從大到小

排除等效冗餘:vis[]

bool dfs (int u, int k) {  //u當前層數,k限制層數
    if (u == k)  //搜到限制那層了
        return path[u - 1] == n;  //如果最後的值是n,那麼表示找到答案了

    memset (vis, false, sizeof vis);  //用於排除等效冗餘
    //從大到小,優化搜尋順序
    for (int i = u - 1; i >= 0; i --)
        for (int j = i; j >= 0; j --) {
            int s = path[i] + path[j];

        //搜過頭了,答案不在此處 || 不滿足逐層擴大的特點 || 等效冗餘
            if (s > n || s <= path[u - 1] || vis[s])
                continue;  

            vis[s] = true;
            path[u] = s;
            if (dfs (u + 1, k))
                return true;
        }
    return false;
}

3. 雙向DFS

useful algo (指二分和暴力/doge)的美妙結合

雙向爆搜,把一半打表(記得去重),另一半在表中二分查詢

  1. 先搜大的
  2. 先將前 k 件物品能湊出的所有重量打表,再排序去重
  3. 搜尋剩下的 n - k 件物品的選擇方式,在表中二分找出不超過 W 的最大值

此題有揹包的思想

// u表示當前列舉到哪個數了, s表示當前的和
void dfs(int u, int s)
{
    // 如果我們當前已經列舉完第k個數(下標從0開始的)了, 就把當前的s, 加到weights中去
    if (u == k) {
        weights[cnt++] = s;
        return;
    }

    // 列舉當前不選這個物品
    dfs(u + 1, s);

    // 選這個物品, 做一個可行性剪枝
    if ((LL)s + g[u] <= m) {  //計算和的時候轉成long long防止溢位
        dfs(u + 1, s + g[u]);
    }
}

void dfs2(int u, int s)
{
    if (u == n) {  // 如果已經找完了n個節點, 那麼需要二分一下
        int l = 0, r = cnt - 1;
        while (l < r) {
            int mid = (l + r + 1) >> 1;
            if (weights[mid] <= m - s)
                l = mid;
            else
                r = mid - 1;
        }
        ans = max(ans, weights[l] + s);
        return;
    }

    // 不選擇當前這個物品
    dfs2(u + 1, s);

    // 選擇當前這個物品
    if ((LL)s + g[u] <= m)
        dfs2(u + 1, s + g[u]);
}

int main()
{
    cin >> m >> n;
    for (int i = 0; i < n; i++)
        cin >> g[i];

    // 優化搜尋順序(從大到小)
    sort(g, g + n);
    reverse(g, g + n);

    k = n / 2 + 2;  // 把前k個物品的重量打一個表
    dfs(0, 0);

    // 做完之後, 把weights陣列從小到大排序
    sort(weights, weights + cnt);

    // 判重
    int t = 1;
    for (int i = 1; i < cnt; i++)
        if (weights[i] != weights[i - 1])
            weights[t++] = weights[i];
    cnt = t;

    // 從k開始, 當前的和是0
    dfs2(k, 0);

    cout << ans << endl;

    return 0;
}

4. IDA*

迭代加深 + 估價函式

迭代加深的基礎上,搜到當前這一步時,估計一下當前點搜到答案所需步數,如果該步數超過限制,就直接剪掉

估價函式 \(\leq\) 真實值

排書

初等數學的魅力

  1. 列舉長度:長度為 i ** 的段有 n - i + 1 種,把這個區間拿出來之後,會剩下 n - i 個數,產生n - i + 1 ** 個空擋,除去自身原本所在地,可放置的空擋就有n - i個。

    所以有(n - i + 1) * (n - i)種選擇。另外,將某一段向前移動,等價於將跳過的那段向後移動,因此每種移動方式被算了兩遍

    \[\sum_{i = 1}^{n}\frac{(n-i+1)(n-i)}{2}=\frac{n(n+1)(n+2)}{3*2} \]
  2. 估價函式:(改變如何體現)更改後繼關係(每次操作變3個)

​ 所以用 tot 統計有多少個不正確的後繼關係,則操作次數\(cnt\)

\[cnt = \lceil \frac{tot}{3}\rceil = \lfloor \frac{tot+2}{3}\rfloor \]
int f() {
    int cnt = 0;  //統計不正確的後繼
    for (int i = 1; i < n; i ++)
        if (a[i] != a[i - 1] + 1)
            cnt ++;

    return (cnt + 2) / 3;
}   //估價函式,每次改變三個後繼

bool dfs (int u, int lim) {
    if (u + f() > lim)
        return false;  //超出最大限度,可行性剪枝

    if (f() == 0)
        return true; //全部後繼都合法了,我滴任務完成啦!

    for (int len = 1; len <= n; len ++)
        for (int i = 0; i < n - len + 1; i ++) {
            int l = i, r = i + len - 1;  
            for (int k = r + 1; k < n; k ++) {
                memcpy (w[u], a, sizeof a);  //備份當前層

                //進行交換操作
                int y = l;
                for (int x = r + 1; x <= k; x ++, y ++)
                    a[y] = w[u][x];
                for (int x = l; x <= r; x ++, y ++)
                    a[y] = w[u][x];

                if (dfs (u + 1, lim))
                    return true;  //合法不?
                
                memcpy (a, w[u], sizeof a);  //回覆
            }
        }
    return false;
}

迴轉遊戲