[POI2014] TUR-Tourism

maniubi發表於2024-09-19

[POI2014] TUR-Tourism

題意

給出一張圖,在這張圖中,任意兩點間不存在節點數超過 \(10\) 的簡單路徑。

\(i\) 個點被選的代價為 \(C_i\),每個節點要麼選,要麼與它直接相連的點中至少有一個被選。

求最小代價。

思路

圖的生成樹上狀壓動態規劃。

由於給出的是一張圖,無法直接dp,我們可以在這張圖的 dfs 樹上dp。

又因為任意兩點間不存在節點數超過 \(10\) 的簡單路徑,

我們可以把每個點到根的路徑上點的狀態進行壓縮。

\(0\):這個點選了。

\(1\):這個點沒選也沒被覆蓋。

\(2\):這個點沒選但被覆蓋。

每次從子節點回收答案,看子節點選還是不選被覆蓋。

每次還需要從父節點繼承答案,列舉返祖邊。

透過祖先的狀態,統計出當前點選和不選時的狀態,從父親的 dp 值繼承過來。

實現時用 \(dp_{i,j}\) 表示深度為 \(i\)(而不是點 \(i\)),狀態為 \(j\) 的最小值,每次清空節省空間。

程式碼和註釋

#define Get(x, y) ((x) / Pow[y] % 3)
#define Set(x, y, v) (x -= Get(x, y) * Pow[y], x += v * Pow[y])
#define min(x, y) ((x) < (y) ? (x) : (y))
const int N = 1e5 + 5;
/*
0:選了
1:不選且未被覆蓋
2:不選且被覆蓋
*/
int ver[N << 1], nxt[N << 1], head[N];
int a[N], n, m, tot;
int dep[N], ans, cnt, z[N];
int dp[11][N], Pow[11];
bool vis[N];
void add(int x, int y) {
    ver[++ tot] = y;   
    nxt[tot] = head[x];
    head[x] = tot;
}
void dfs(int x) {
    vis[x] = 1, cnt = 0;
    for (int i = head[x], y; i; i = nxt[i]) {
        y = ver[i];
        if (vis[y]) z[++ cnt] = dep[y]; // 返祖邊連向的祖先
    }
    if (dep[x] == 0) { // 根節點
        dp[0][0] = a[x];
        dp[0][1] = 0;
        dp[0][2] = 1e9;
    } else {
        for (int i = 0; i < Pow[dep[x] + 1]; i ++) dp[dep[x]][i] = 1e9; // 賦值
        for (int i = 0; i < Pow[dep[x]]; i ++) { // 列舉父親狀態
            int yes = i, no = i; 
            // yes:當前點選後的狀態
            // no:當前點不選後的狀態
            Set(no, dep[x], 1); // 初始時就是不選未覆蓋 
            for (int j = 1; j <= cnt; j ++) {
                int y = z[j];
                if (Get(i, y) == 0) Set(no, dep[x], 2); // 如果有祖先選了,當前點變為未選被覆蓋
                if (Get(i, y) == 1) Set(yes, y, 2); // 如果有祖先不選未覆蓋,把它變為未選被覆蓋
            }
            dp[dep[x]][no] = min(dp[dep[x]][no], dp[dep[x] - 1][i]); // 沒選當前點,直接繼承
            dp[dep[x]][yes] = min(dp[dep[x]][yes], dp[dep[x] - 1][i] + a[x]); // 選了當前點,加上權值
        }
    }
    for (int i = head[x], y; i; i = nxt[i]) {
        y = ver[i];
        if (vis[y]) continue;
        dep[y] = dep[x] + 1;
        dfs(y);
        for (int i = 0; i < Pow[dep[y]]; i ++) 
            dp[dep[x]][i] = min(/*選*/dp[dep[y]][i], /*不選被覆蓋*/dp[dep[y]][i + 2 * Pow[dep[y]]]); // 從兒子回收答案 
    }
}
int main() {
    memset(dp, 0x3f, sizeof(dp));
    Pow[0] = 1;
    for (int i = 1; i <= 10; i ++)
        Pow[i] = Pow[i - 1] * 3;
    read(n), read(m);
    for (int i = 1; i <= n; i ++) read(a[i]);
    for (int i = 1, u, v; i <= m; i ++) {
        read(u), read(v);
        add(u, v);
        add(v, u);
    }
    for (int i = 1; i <= n; i ++) {
        if (vis[i]) continue;
        dfs(i);
        ans += min(dp[0][0], dp[0][2]);
    }
    print(ans);
    print_final();
    return 0;
}