P11188 解題報告

Brilliant11001發表於2024-10-16

題目傳送門

分享一下我做這道題是的心路歷程。

首先感覺像是貪心,但是隨便舉了幾個例子就推翻了,發現無論是先刪掉 \(v\) 值小的,還是先刪掉靠前且數值大的都不行。

策略的選擇如此複雜,考慮 dp。

其實很容易就能發現資料範圍的異樣:\(v_i\le 10^5\),這告訴我們操作 \(2\) 最多隻能操作最後的 \(5\) 個數。

因為 \(5\times 10^5>10^5\),而 \(6\times 10^5<10^6\),所以選超過 \(5\) 個數進行操作二一定不如操作一優。

自此,我們可以將題意轉化為:

給定一個序列 \(s\),從 \(s\) 中選出一個子序列 \(\{a_1, a_2,\cdots,a_k\}\),使得 \(\overline{a_1a_2\cdots a_k} - \sum\limits_{i = 1}^kv_i\) 最小。

dp 的狀態設計其實可以參考資料範圍,設 \(n\) 為原數的長度,考慮狀態設計為 \(dp_{i, j}\)

一開始我想的是直接線性 dp,從前向後遞推,同時記錄下此時最優策略保留下來的數來輔助遞推,但很快就發現連樣例 \(2\) 的第一組資料都過不了。

錯誤 \(\texttt{Code:}\)

#include <cstring>
#include <iostream>

#define x first
#define y second

using namespace std;

const int N = 100010;
typedef long long ll;
typedef pair<ll, ll> PLL;
int cid, T;
int n;
char s[N];
int v[10];
PLL dp[N][6];

void solve() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    for(int i = 1; i < 10; i++) scanf("%d", &v[i]);
    ll sumcost = 0;
    for(int i = 1; i <= n; i++)
        sumcost += v[s[i] - '0'];
    for(int i = 0; i <= n; i++)
        for(int j = 0; j <= 5; j++)
            dp[i][j] = {1e18, 0};
    dp[0][0] = {0, 0};
    for(int i = 1; i <= n; i++) {
        int limit = min(i, 5);
        dp[i][0] = dp[i - 1][0];
        for(int j = 1; j <= limit; j++) {
            dp[i][j] = dp[i - 1][j];
            if(dp[i][j].x > dp[i - 1][j - 1].x + dp[i - 1][j - 1].y * 10 + s[i] - '0' - v[s[i] - '0'])
                dp[i][j] = {dp[i - 1][j - 1].x + dp[i - 1][j - 1].y * 10 + s[i] - '0' - v[s[i] - '0'], dp[i - 1][j - 1].y * 10 + s[i] - '0'};
        }
    }
    ll ans = 1e18;
    for(int i = 0; i <= min(5, n); i++)
        ans = min(ans, dp[n][i].x);
    printf("%lld\n", sumcost + ans);
}

int main() {
    scanf("%d%d", &cid, &T);
    while(T--) {
        solve();
    }
    return 0;
}

然後我就發現:正著 dp 是錯的。

因為你正著 dp 的時候只會保留當前長度下最優的解,但它實際上是具有後效性的。

就是這組樣例:

3158927982863528
41423 65081 37768 31661 5606 86055 71796 46535 92370

最優的策略是保留 \(19796\) 最後刪,其餘全用方法一刪掉。

但如果正著這樣 dp,那麼當考慮前 \(8\) 位時,最優解是刪掉 \(9796\)而刪掉 \(1979\) 這個策略的就被覆蓋掉了。但事實上最後用後者更新的答案是要比前者更優的。

但是倒著 dp 就沒有後效性了!

原因就是如果倒著做,每次更新時直接加上 \(num_i\times 10^{j - 1}\) 再減去 \(v_{num_i}\) 就行了,各個貢獻的計算是獨立進行的。

那麼新的狀態定於就是:\(dp_{i, j}\) 表示從後往前考慮到第 \(i\) 位時,保留了 \(j\) 個數時的最小 \(num - \sum v\)

狀態轉移方程:

\(dp_{i, j} = \min\{dp_{i + 1, j}, dp_{i + 1, j - 1} + num_i\times 10^{j - 1} - v_{num_i}\}\)

最後答案就是選取 \(0\sim 5\) 個留下來的最小值,加上全用方法一消除的代價。

\(\texttt{AC Code:}\)

#include <cstring>
#include <iostream>

#define x first
#define y second

using namespace std;

const int N = 100010;
typedef long long ll;
int cid, T;
int n;
char s[N];
int v[10];
ll dp[N][6];
int power10[10];

void solve() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    for(int i = 1; i < 10; i++) scanf("%d", &v[i]);
    ll sumcost = 0;
    for(int i = 1; i <= n; i++)
        sumcost += v[s[i] - '0'];
    for(int i = 0; i <= n + 1; i++)
        for(int j = 0; j <= 5; j++)
            dp[i][j] = 1e18;
    dp[n + 1][0] = 0;
    for(int i = n; i; i--) {
        dp[i][0] = dp[i + 1][0];
        int limit = min(5, n - i + 1);
        for(int j = 1; j <= limit; j++) {
            dp[i][j] = dp[i + 1][j];
            dp[i][j] = min(dp[i][j], dp[i + 1][j - 1] + (s[i] - '0') * power10[j - 1] - v[s[i] - '0']);
        }
    }
    ll ans = 1e18;
    for(int i = 0; i <= min(5, n); i++)
        ans = min(ans, dp[1][i]);
    printf("%lld\n", sumcost + ans);
}

int main() {
    scanf("%d%d", &cid, &T);
    power10[0] = 1;
    for(int i = 1; i < 10; i++)
        power10[i] = power10[i - 1] * 10;
    while(T--) {
        solve();
    }
    return 0;
}

相關文章