日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

状态压缩DP AcWing算法提高课 (详解)

發布時間:2025/3/19 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 状态压缩DP AcWing算法提高课 (详解) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

基礎課的狀態壓縮點這里

  • 基礎課中 蒙德里安的夢想 屬于 棋盤式狀態壓縮dp,最短Hamilton路徑 屬于 集合狀態壓縮dp

1064. 小國王(棋盤式/基于連通性)

  • 這種棋盤放置類問題,在沒有事先知道一些特定 性質 的情況下來做,都會想到 爆搜,本題的數據規模,也是向著 爆搜 去設置的,如果我們直接 爆搜,則 時間復雜度 為O(2n2)O(2^{n^2})O(2n2)是會超時的,因此會想到用 記憶化搜索 來進行優化
  • 發現對于當前行,只與上一行有關,與上上行無關
  • f[i,s]f[i,s]f[i,s]表示前i行已經做完了,最后一行的狀態是s的總共的方案數
  • 這里還有對放的棋子個數的限制,所以還要加一維表示當前已經放了多少個棋子,f[i,j,s]f[i,j,s]f[i,j,s]
  • 狀態表示-集合 :f[i,j,s]f[i,j,s]f[i,j,s]表示所有只擺在前i行,已經擺了j個國王,并且第i行擺放的狀態是s的所有方案的集合
  • 狀態表示-屬性 :count(數量)
  • 狀態計算(集合劃分的過程):
    1.去看最后一步不同來劃分,可知第i-1行最多有2n2^n2n種擺放方式,也就是指數級
    2.有兩個需要滿足的條件:1.第i-1行內部不能有兩個1相鄰;2.第i行和第i-1行的國王之間不能相互攻擊到
    3.用代碼翻譯就是,假設分別的二進制狀態是a和b。那么,因為a和b不能同時是1,所以(a & b) == 0;(a | b)表示綜合看兩行的話哪些列是有國王的,那么(a | b)中不能有兩個相鄰的1就使得綜合兩行沒有兩個國王所在列相鄰;滿足以上兩個條件就是滿足2中的第二個性質;只要滿足了性質,直接加上這一類中的元素數量,現在的狀況是“已經擺完了前排,并且第i排的狀態是a,第i-1排狀態是b的所有方案,已經擺了j個國王“,去掉最后一排,“已經擺完第i-1排,并且第i-1排的狀態是b,已經擺了j-count(a)個國王的所有方案“
  • 時間復雜度 :狀態數量 * 狀態計算的計算量,就是10 * 100 * S * Sh,最壞的情況下S有1000,每個狀態又有1000次狀態轉移,所以10 * 100 * 1000 * 1000,即1^9,那么為什么可以做呢,我們可以通過預處理所有的 合法狀態,以及合法的 相鄰轉移狀態,以及 合法狀態 擺放的國王數量,因為雖然狀態很多,但是 合法狀態 并不多, 合法的轉移狀態 更不多;狀態壓縮dp的特點 :用最暴力的方式算時間復雜度,都會超過時間上限
  • 最后輸出答案時還有一個偷懶小技巧,從f[n][m]...f[n][m]...f[n][m]...這種需要循環一次的換成f[n+1][m][0]f[n+1][m][0]f[n+1][m][0]直接輸出即可,對應的在前面狀態轉移的循環中多循環一次,然后就是N也要開成12
  • 還有一個小技巧,這里head(由這個狀態可以轉移到哪些狀態)里存的是state中的下標,而不是具體狀態,狀態表示中也是下標而不是具體二進制值
  • 注意不要寫成(a & b == 0),是(a & b) == 0
#include <iostream> #include <algorithm> #include <cstring> #include <queue> #include <vector> #define debug(a) cout << #a << " = " << a << endl; //#define x first //#define y second #define LD long double using namespace std; typedef long long ll;const int N = 12, M = 1 << N, K = 110;int n, m; vector<int> state; vector<int> head[M]; int cnt[M]; ll f[N][K][M];bool check(int state) {for (int i = 0; i < n; i ++ )if ((state >> i & 1) && (state >> (i + 1) & 1)) return false;return true; }int count(int state) {int cnt = 0;for (int i = 0; i < n; i ++ )cnt += (state >> i & 1);return cnt; }int main() {cin >> n >> m;for (int i = 0; i < (1 << n); i ++ )if (check(i)){state.push_back(i);cnt[i] = count(i);}for (int i = 0; i < state.size(); i ++ )for (int j = 0; j < state.size(); j ++ ){int a = state[i], b = state[j];if ((a & b) == 0 && check(a | b)) // 不要寫成head[i].push_back(j);}f[0][0][0] = 1;for (int i = 1; i <= n + 1; i ++ )for (int j = 0; j <= m; j ++ )for (int a = 0; a < state.size(); a ++ )for (int b : head[a]){int c = cnt[state[a]];if (j >= c)f[i][j][a] += f[i - 1][j - c][b];}cout << f[n + 1][m][0] << endl;return 0; }

327. 玉米田

  • 上一題是井字型不能種,這題是十字型
  • 同樣,這里一行只與上一行有關,與上上行無關,所以只要用二進制表示上一行的擺放情況即可
  • 狀態表示-集合 :f[i,s]f[i, s]f[i,s]表示 所有已經擺完前i行,且第i行的狀態是s的所有擺放方案的 集合
  • 狀態表示-屬性 :count
  • 狀態計算 :滿足兩個性質 :1.a,b的二進制表示中不包含兩個連續的1;2.(a&b)==0
  • 時間復雜度 :n?2n?2nn*2^n*2^nn?2n?2n,即12?22412*2^{24}12?224,但,所以過
  • 這題還有一個特殊之處,有些田是不能種的,這個可以通過定義一個數組g[N],g[i]表示第i行能否種田的二進制表示,1表示不能種(我們要舍棄的是田不能種且同時state在這個田種了的情況(1),容易想到&的性質,所以將田不能種定義為1),然后在狀態計算時g[i] & state[a]如果不為0,說明有同時為1的一位,則這個狀態不合法,continue,否則就一定合法
  • 狀態計算時不需要判斷f[i-1][b]是否合法,因為如果不合法,在i-1的時候就已經被continue了
#include <iostream> #include <algorithm> #include <cstring> #include <queue> #include <vector> #define debug(a) cout << #a << " = " << a << endl; //#define x first //#define y second #define LD long double using namespace std; typedef long long ll;const int N = 14, M = 1 << 12, mod = 1e8;int n, m; int g[N]; vector<int> state; vector<int> head[M]; int f[N][M];bool check(int state) {for (int i = 0; i < m; i ++ )if ((state >> i & 1) && (state >> i + 1 & 1)) return false;return true; }int main() {cin >> n >> m;for (int i = 1; i <= n; i ++ )for (int j = 0; j < m; j ++ ){int t;cin >> t; // g[i] += !t << j;g[i] += !t * (1 << j);}for (int i = 0; i < 1 << m; i ++ ) // 注意m列,不是n列if (check(i))state.push_back(i);for (int i = 0; i < state.size(); i ++ )for (int j = 0; j < state.size(); j ++ )if (!(state[i] & state[j]))head[i].push_back(j);f[0][0] = 1;for (int i = 1; i <= n + 1; i ++ ) // 到n + 1for (int j = 0; j < state.size(); j ++ )if (!(g[i] & state[j])){for (int k : head[j])f[i][j] = (f[i][j] + f[i - 1][k]) % mod;}cout << f[n + 1][0] << endl;return 0; }

292. 炮兵陣地


  • 與上一題區別 :1.射程變化了,從一格變成兩格了;2.求的從方案數變成了最多擺放的數量。大同小異
  • 本題不同于 小國王 和 玉米田,這兩題中棋子的 攻擊距離 只有1,因此在這兩題里,我們只需壓縮存儲 當前層的狀態 ,然后枚舉 合法的上個階段 的狀態進行 轉移 即可,但是本題的棋子攻擊范圍是 2,我們只壓縮當前層一層狀態后進行轉移,是不能保證該次轉移是 合法的,即不能排除第 i?2 層擺放的棋子可以攻擊到第 i 層棋子的 不合法 情況,而解決該問題的手段就是:壓縮存儲兩層的信息,然后枚舉合法的第 i?2 層狀態進行轉移即可
  • 考慮當前一行擺放的狀態時,不僅和上一行有關,還與上上行有關,因此,只要在狀態里再加一維即可
  • 狀態表示-集合 :f[i,j,k]f[i,j,k]f[i,j,k]表示 所有已經擺放完前i行,且第i-1行擺放的狀態是j,第i行擺放的狀態是k的所有擺放方案的最大值
  • 狀態表示-屬性 :max
  • 狀態計算 :
    1.在分析f[i,j,k]劃分子集時,已經確定第i-1行是j,第i行是k了
    2.第i-1行是a,第i行是b,第i-2行是c
    3.需要滿足的條件是 :1.abc兩兩之間不會相互攻擊到,也就是兩兩之間不能有同一列上都有炮,也就是a&b==0&&a&c==0&&b&c==0,即((a & b) | (a & c) | (b & c)) == 0;2.炮不能放山地上,即g[i-1]&a==0&&g[i]&b==0,即(g[i - 1] & a | g[i] & b) == 0
  • 時間復雜度 :n?2m?2m?2mn*2^{m}*2^{m}*2^{m}n?2m?2m?2m,即100?230100*2^{30}100?230,是101110^{11}1011,非常恐怖,但這里所有相鄰兩個1之間至少隔兩個0,而前面只是至少隔1個0,有效狀態數更少,合法的狀態和合法的轉移都很少
  • 這題還有一個問題,空間限制比較小,所以要用滾動數組,先當成不滾動來寫,然后把所有第一維與上1,就可以變成滾動數組了。滾動數組就是i和i-1一定是一個奇數一個偶數,&上1后肯定是0和1交替的。關于滾動數組,這題中狀態表示屬性是max,所以不需要清空,像前面兩題求方案數,每次都枚舉完狀態的每一維開始狀態轉移前都要進行清空(判斷前)
  • 易錯點,輸入地圖的時候用char讀入,而不是int
  • 注意到前面兩題由于只與上一行有關,所以在有效狀態的預處理后還有一個表示從這個狀態可以合法狀態轉移到的狀態的預處理,而這里由于與前兩行都有關,就沒有這個預處理了,而是在狀態轉移計算的時候判斷是否合法
#include <iostream> #include <algorithm> #include <cstring> #include <queue> #include <vector> #define debug(a) cout << #a << " = " << a << endl; //#define x first //#define y second #define LD long double using namespace std; typedef long long ll;const int N = 11, M = 1 << 10;int n, m; int g[110]; int f[2][M][M]; // 滾動數組 int cnt[M]; vector<int> state;bool check(int state) {for (int i = 0; i < m; i ++ )if ((state >> i & 1) && ((state >> i + 1 & 1) || (state >> i + 2 & 1)))return false;return true; }int count(int state) {int cnt = 0;for (int i = 0; i < m; i ++ )cnt += state >> i & 1;return cnt; }int main() {cin >> n >> m;for (int i = 1; i <= n; i ++ )for (int j = 0; j < m; j ++ ){char c;cin >> c;g[i] += (c == 'H') * (1 << j);}for (int i = 0; i < (1 << m); i ++ )if (check(i)){state.push_back(i);cnt[i] = count(i);}for (int i = 1; i <= n + 2; i ++ ) // 特殊技巧for (int j = 0; j < state.size(); j ++ )for (int k = 0; k < state.size(); k ++ )for (int u = 0; u < state.size(); u ++ ){int a = state[j], b = state[k], c = state[u];if (g[i - 1] & a | g[i] & b) continue;if (a & b | a & c | b & c) continue;f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[b]); // 注意最后加上的是cnt[b]而不是cnt[k]}// 不使用特殊技巧的情況 // int res = 0; // for (int i = 0; i < state.size(); i ++ ) // for (int j = 0; j < state.size(); j ++ ) // res = max(res, f[n & 1][i][j]);cout << f[n + 2 & 1][0][0] << endl; // 特殊技巧return 0; }

524. 憤怒的小鳥(集合,重復覆蓋問題)


  • 拋物線y=ax2+bx+cy=ax^2+bx+cy=ax2+bx+c,且根據題意滿足兩個條件:1.a<0;2.過原點,c=0 -> 即只需兩個點(且這兩個點不在一條豎線上x1!=x2x_1 != x_2x1?!=x2?),即可確定此拋物線

  • 一共有n個點,因此最多有n2n^2n2條拋物線,先預處理所有拋物線,還要預處理這些拋物線能夠覆蓋的點有哪些。這個問題轉化為給定若干條拋物線,問最少選擇多少條拋物線可以覆蓋所有點,這是一個經典的重復覆蓋問題,這里用集合類型的狀態壓縮dp去優化爆搜

  • 如果用爆搜怎么做呢?爆搜的核心是順序(下圖中圖一的爆搜方案被pass了)

  • 怎么考慮優化呢 ?上述dfs中state與返回的res肯定是一一對應的,所以用f[state]f[state]f[state]存儲res,避免重復計算,即記憶化搜索

  • 根據兩個點如何推拋物線公式 :

  • 存二維坐標的常用技巧:typedef pair和x替代first

  • 有些點可能無法與其它任何點共用一條拋物線的(因為要滿足a<0),所以要先path[i][i] = 1 << j

  • 一個易錯點 :比較兩個浮點數,由于浮點數是存在誤差的,所以比較兩個浮點數時需要考慮誤差,可能兩個浮點數相等,但存的時候由于一些計算導致它們值相差了一個很少的數,所以要加上一個誤差的容忍度

  • f[0] = 0,0這個位置不需要任何拋物線,就比如dfs時調用的也是dfs(0, 0)

  • 所有能覆蓋x的拋物線就是path[x][j]

  • fabs在頭文件cmath中

  • 狀態轉移時枚舉狀態是到(1 << n) - 1

  • 狀態轉移時每次只找一個最低位未被覆蓋的點

#include <iostream> #include <cstring> #include <cmath>#define x first #define y secondusing namespace std;typedef pair<double, double> PDD;const int N = 18, M = 1 << 18; const double eps = 1e-8;int n, m; PDD q[N]; int path[N][N]; int f[M];int cmp(double x, double y) // int 不是bool {if (fabs(x - y) < eps) return 0;if (x < y) return -1;return 1; }int main() {int _;cin >> _;while (_ -- ){cin >> n >> m;for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y;memset(path, 0, sizeof path);for (int i = 0; i < n; i ++ ){path[i][i] = 1 << i; // 應對特殊點for (int j = 0; j < n; j ++ ){double x1 = q[i].x, y1 = q[i].y;double x2 = q[j].x, y2 = q[j].y;if (!cmp(x1, x2)) continue; // x1 == x2 continuedouble a = (y1 / x1 - y2 / x2) / (x1 - x2);double b = y1 / x1 - a * x1;if (cmp(a, 0) >= 0) continue; // a >=0 continueint state = 0;for (int k = 0; k < n; k ++ ){double x = q[k].x, y = q[k].y;if (!cmp(a * x * x + b * x, y))state += 1 << k;}path[i][j] = state;}}memset(f, 0x3f, sizeof f); // 求minf[0] = 0;for (int i = 0; i + 1 < 1 << n; i ++ ) // 求到(1 << n) - 2即可得到最后的(1 << n) - 1{int x = 0;for (int j = 0; j < n; j ++ )if (!(i >> j & 1)){x = j;break;}for (int j = 0; j < n; j ++ )f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);}cout << f[(1 << n) - 1] << endl;}return 0; }

529. 寶藏



題意 :

  • 給定一個n個點,m條邊且連通的 無向圖
  • 初始時,無向圖中沒有邊,每個 都是 互不連通
  • 我們初始可以選擇任意一個點作為 起點,且該選擇 起點 的操作 費用 是0
  • 然后開始維護這個包含起點的 連通快
  • 每次我們選擇一個與 連通快內的點 相連的一條邊,使得該邊的另一個點(本不在連通快內的點)加入連通快
  • 該次選邊操作的費用是:邊權?該點到起點的簡單路徑中經過的點數邊權*該點到起點的簡單路徑中經過的點數?
  • 最終我們的目標是,使得 所有點都加入當前的連通快內
  • 求解一個 方案,在達成目標的情況下,費用最小,輸出方案的費用

思路 :

  • 題目要求已經連通的兩點之間不需要連接額外的邊(其實可以直接被推出),即我們只需要選擇n - 1條邊即可
  • 因此,我們的目標是 找出圖中的 最小生成樹,但本題的最小生成樹 和廣義的最小生成樹不一樣,因為在本題中,加入連通快的費用是隨當前點到起點的路徑線性變化的(設經過的點數為k,則費用為x?kx*kx?k
  • 計算該次加邊的費用是用到了如下兩個參數:
    1.起點
    2.當前點在當前生成樹上到起點的簡單路徑上經過的點數
  • 如果恰好是只有其中一項限制,我們都可以直接套最小生成樹的模版改一改即可 :
    1.只有起點限制,我們可以暴力枚舉起點然后套Prim即可O(n3)O(n^3)O(n3)
    2.只有當前生成樹的狀態限制,我們可以額外開一個數組,記錄加入生成樹的點到起點經過的點數,然后dfs O(22n)O(2^{2n})O(22n)
  • 然而本題這兩個參數都要考慮,就不能直接套模版來改了(不優化的話,時間復雜度要到O(n22n)O(n2^{2n})O(n22n)
  • 因此我們不妨采用 爆搜優化 -> 記憶化搜索 -> 動態規劃 來解決該問題
  • 我們把當前 生成樹 的狀態作為 DP 的階段,那么需要額外記錄的參數就是樹的 深度,記錄深度之后,可以保證我們枚舉下一個階段的狀態時,能夠輕易的計算他到起點的路徑經過的點數,即我們把起點作為樹的 root 然后一層一層構造 生成樹
  • 時間復雜度 :
    1.預處理 O(n2?2n)O(n^2*2n)O(n2?2n)
    2.動態規劃 O(n2?3n)O(n^2*3^n)O(n2?3n),見如下分析
#include <iostream> #include <cstring>using namespace std;const int N = 15, M = 1 << N, K = 1010; const int INF = 0x3f3f3f3f;int n, m; int g[N][N]; int f[M][K]; int ne[M];void init() {cin >> n >> m;// 圖的初始化memset(g, 0x3f, sizeof g);for (int i = 0; i < n; ++ i) g[i][i] = 0;while (m -- ){int a, b, c;cin >> a >> b >> c;a -- , b -- ; // 狀態壓縮dp,所以編號從0開始g[a][b] = g[b][a] = min(g[a][b], c);}for (int st = 1; st < 1 << n; ++ st) // 預處理所有狀態能夠擴展一層后得到的最大的下一層狀態for (int i = 0; i < n; ++ i)if (st >> i & 1)for (int j = 0; j < n; ++ j)if (g[i][j] != INF)ne[st] |= 1 << j; }int get_cost(int cur, int pre) {if ((ne[pre] & cur) != cur) return -1; // 如果pre延伸不到cur直接剪枝int remain = pre ^ cur; // 待更新的方案int cost = 0; // 記錄邊權總和for (int i = 0; i < n; ++ i)if (remain >> i & 1){int t = INF;for (int j = 0; j < n; ++ j)if (pre >> j & 1)t = min(t, g[i][j]);cost += t;}return cost; // 返回該次擴展的費用 }int dp() {memset(f, 0x3f, sizeof f);for (int i = 0; i < n; ++ i) f[1 << i][0] = 0; // 開局免費選一個起點(初始狀態)for (int cur = 1, cost; cur < 1 << n; ++ cur)for (int pre = cur & cur - 1; pre; pre = pre - 1 & cur)if (~(cost = get_cost(cur, pre)))for (int k = 0; k < n; ++ k)f[cur][k] = min(f[cur][k], f[pre][k - 1] + cost * k);int res = INF;for (int k = 0; k < n; ++ k) res = min(res, f[(1 << n) - 1][k]);return res; }int main() {init();cout << dp() << endl;return 0; }

總結

以上是生活随笔為你收集整理的状态压缩DP AcWing算法提高课 (详解)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。