CF1238E.Keyboard Purchase 題解 狀壓/子集劃分DP

  • 做者:zifeiy
  • 標籤:狀壓DP,子集劃分DP

題目連接:https://codeforces.com/contest/1238/problem/E
題目大意:
給你一個長度爲 \(n(n \le 10^5)\) 的字符串s和 \(m(m \le 20)\) ,這個字符串由前 \(m\) 個小寫字母組成。
如今你要找一個前 \(m\) 個字符的一個排列p,在這個排列p的基礎上生成字符串s,並計算總代價。
代價的計算過程是:
好比我如今已經生成了字符串s的前i個字符 \(s_{1..i}\) ,如今我要生成第i+1個字符 \(s_{i+1}\) ,而且我假設 \(s_i\) 對應的字符是x,\(s_{i+1}\) 對應的字符是y,而且字符x在排列p中的位置是 \(pos_x\) ,字符y在排列p中的位置是 \(pos_y\) ,那麼生成了字符 \(s_{i+1}\) 以後,總代價增長了 \(| pos_x - pos_y |\)
你須要找到全部排列方案中總代價最小的那個方案所對應的最小總代價。c++

題目分析:
首先能夠看一下 官方題解
函數

官網的解釋當中涉及到了一個「subset dynamic programming」,我粗略地將它翻譯爲「子集動態規劃」,其實能夠發現這道題目的1真的跟子集有一些練習。
同時它跟狀態壓縮也有一些關係。
咱們用i來表示每個狀態(\(0 \le i \lt 2^m\)),這個i其實對應成一個二進制數就是一個 m 位的二進制數,若是i的第j位爲1就說明這個狀態中已經放了第j個字符,不然就說明沒有放第j個字符,那麼咱們就能夠放第j個字符,也就是說:
經過狀態 i 和字符 j 可以擴展出一個新的狀態 i | (1<<j)
並且 j 放的位置也是肯定的——咱們能夠用函數 __builtin_popcount(i) 來獲取 i 的二進制表示種存在多少位爲1。
而後咱們這裏我以爲最重要的一個點也是困擾了我好久的一個點是—— 「如何消除位置的影響」
其實對於每個狀態 i 和字符 j ,若是想要從狀態 i 轉移到狀態 i | (1<<j) ,而且咱們假設 i 的二進制表示中有 c 位爲1,那麼咱們其實能夠發現,j所處的排列的位置是肯定的,那就是 c (也能夠是c+1,這個視你的初始座標決定,咱們這裏就假設爲c了)。
可是到目前爲止咱們還不能消除距離的影響。
咱們能夠枚舉狀態i裏面的第k位:ui

  • 若是狀態i的第k位爲1,那麼也就是說字符k以前已經放在了狀態i的某一個位置(咱們假設是 \(c_k\)),
    那麼如今咱們要在第c個位置放j(爲了清晰起見,咱們令 \(c_j\) 表示 c),
    那麼k和j的代價應該是 \(cnt[j][k] \times (c_j - c_k)\) ;(由於 \(c_k \lt c_j\)
  • 若是狀態i的第k位爲0,那麼也就是說字符k尚未狀態i的某一個位置(咱們假設是 \(c_k\)),
    那麼如今咱們要在第c個位置放j(爲了清晰起見,咱們令 \(c_j\) 表示 c),
    那麼k和j的代價應該是 \(cnt[j][k] \times (c_k - c_j)\) 。(由於 \(c_j \lt c_k\)

因此,咱們能夠發現,若是我令 \(f[i]\) 表示i這個狀態的最小總代價,
那麼,當咱們當前判斷狀態i和字符j的時候(而且咱們假設i的第j位爲0,由於此時咱們能夠將狀態 i 加上字符 j 變到一個新的狀態 i | (1<<j))。
咱們一開始開一個變量 \(tmp = f[i]\) ,而後從 0 到 m-1 去遍歷字符k:spa

  • 若是k已經存在在狀態i中,則 \(tmp -= cnt[k][j] \times c\)
  • 若是k尚未存在在狀態i中,則 \(tmp += cnt[k][j] \times c\) 。(這裏的c就是以前所說的字符j放到狀態i中的位置)

那麼這樣爲何消除了位置的影響呢?
咱們假設如今放j的時候尚未放k,則咱們的tmp變量減去了 \(cnt[k][j] \times c_j\) ,那麼到以後去放k時候,咱們的tmp變量還會加上 \(cnt[k][j] \times c_k\) ,而 \(c_k - c_j\) 其實就是它們的距離,這麼一加一減就間接地處理了距離(這個點困惑了我好長時間,直到豁然開朗!)。翻譯

而後對於每個狀態i和字符j(要求狀態i中的第j位爲0),以及計算獲得的tmp:code

f[ i | (1<<j) ] = max(f[ i | (1<<j) ] , tmp);

實現代碼以下:blog

#include <bits/stdc++.h>
using namespace std;
int n, m, f[(1<<20)], cnt[20][20];
string s;
int main() {
    cin >> n >> m >> s;
    for (int i = 1; i < n; i ++) {
        int a = s[i-1] - 'a', b = s[i] - 'a';
        if (a != b) {
            cnt[a][b] ++;
            cnt[b][a] ++;
        }
    }
    fill(f+1, f+(1<<m), INT_MAX);
    for (int i = 0; i < (1<<m)-1; i ++) {
        int c = __builtin_popcount(i);
        for (int j = 0; j < m; j ++) {
            if ( !( i & (1<<j) ) ) {
                int tmp = f[i];
                for (int k = 0; k < m; k ++) {
                    if (i & (1<<k)) {
                        tmp += cnt[j][k] * c;
                    }
                    else { // 由於預處理cnt的時候保證cnt[x][x]==0
                        tmp -= cnt[j][k] * c;
                    }
                }
                f[ i | (1<<j) ] = min(f[i | (1<<j)], tmp);
            }
        }
    }
    cout << f[ (1<<m)-1 ] << endl;
    return 0;
}
相關文章
相關標籤/搜索