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; } } }
武士風度的牛
抓住那頭牛
這倆都是同一型別的簡單題
3. 多源BFS
只更新一次。反著來,通過1更新0
4. 最小步數模型
稍顯煩人的模擬
5. 雙端佇列廣搜
無向圖,邊權為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,就代表可以放這個數字
木棍
- 列舉sum的約數(保證能被整除)
- 優化搜尋順序:先列舉長的木棍
- 排除等效冗餘:
- 按照組合數的方式來列舉
- 與已經失敗的木棍長度相同所有 的一定也不行
- 如果某木棒放第一根木棍u導致當前這根木棒湊不成length,整個方案一定失敗
- 如果木棒的最後一根木棍 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)的美妙結合
雙向爆搜,把一半打表(記得去重),另一半在表中二分查詢
- 先搜大的
- 先將前 k 件物品能湊出的所有重量打表,再排序去重
- 搜尋剩下的 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\) 真實值
排書
-
列舉長度:長度為 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} \] -
估價函式:(改變如何體現)更改後繼關係(每次操作變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;
}