「REMAKE系列」线性dp篇

常见模型、技巧总结

LIS、LCS模型

LIS

LCS

有趣线性dp

  • 定义状态为修改中。 CF1155D

习题

洛谷——「能力综合提升题单-线性DP篇」

P2501 [HAOI2006]数字序列

省选/NOI- LIS、结论

现在我们有一个长度为 \(n\) 的整数序列 \(a\)。但是它太不好看了,于是我们希望把它变成一个单调严格上升的序列。但是不希望改变过多的数,也不希望改变的幅度太大。
第一行输出一个整数,表示最少需要改变多少个数。
第二行输出一个整数,表示在改变的数最少的情况下,每个数改变的绝对值之和的最小值。

  • 对于 \(100\%\) 的数据,保证 \(1 \leq n \leq 3.5 \times 10^4\)\(1 \leq a_i \leq 10^5\)。数据保证 \(a_i\) 随机生成。

思路

  • 结论题两个结论,数据随机 \(O(n^2)\) 可以过。
  • 对于第一问,构造 \(b[i] = a[i] - i\),求 b[] 的最长单调不减子序列长度即可。
  • 对于第二问。考虑 b[] 中最长单调不减子序列中的相邻元素下标为 i, j,对应下标之间的大小一定不在 [b[i], b[j]] 之间,否则子序列可以更长。
    • 那么对于每一对相邻元素(序列不止一个),最终的结果一定是中间某一个 \(k(i\leq k \leq j)\), 使得 b[i..k] = b[i], b[k+1...j]=b[j],这样的结果是最优的。
    • 所以对于每一对相邻元素的区间,采用 dp 转移,设 dp[i] 表示处理以下标 \(i\) 为结尾的最长单调不减子序列最小代价
    • 枚举分界点 k。判断相邻元素区间合法,在 LIS 时记录每个下标作为末尾对应的最长长度。
const int N = 4e4 + 10;
int a[N], n, b[N], v[N];
ll dp[N];
ll s[N], suf[N], L[N];
vector<int> pos[N];

int main() {
    re(n);
    for (int i = 1; i <= n; i++)
        re(a[i]), b[i] = a[i] - i;
    b[0] = -2e9;
    b[n + 1] = 2e9;     // 最后一个(段)元素也是会被修改的,所以将边界搞到 n + 1
    int len = 0;
    for (int i = 1; i <= n + 1; i++) {
        int l = 0, r = len + 1;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (b[v[mid]] > b[i]) r = mid;
            else l = mid + 1;
        }
        if (l == len + 1) v[++len] = i;
        else v[l] = i;
        L[i] = l;
        pos[l].pb(i);
    }
    int ans1 = n - len + 1;
    memset(dp, 0x3f, sizeof dp);
    dp[0] = 0;
    pos[0].pb(0);
    for (int i = 1; i <= n + 1; i++) {
        for (auto pre: pos[L[i] - 1]) {
            if (pre > i || b[pre] > b[i]) continue;
            s[pre] = 0;
            suf[i - 1] = 0;
            for (int j = pre + 1; j <= i - 1; j++) {
                s[j] = s[j - 1] + abs(b[j] - b[pre]);
            }
            for (int j = i - 2; j >= pre; j--)
                suf[j] = suf[j + 1] + abs(b[j + 1] - b[i]);
            for (int k = pre; k <= i - 1; k++) {    // pre, k --- k + 1, i
                dp[i] = min(dp[i], dp[pre] + s[k] + suf[k]);
            }
        }
    }
    printf("%lld\n%lld\n", ans1, dp[n + 1]);
    return 0;
}

Codeforces

CF1155D. Beautiful Array

*1900 定义状态为修改中

定义三个状态,未修改、正在修改、修改结束了,相互转移即可

const int N = 3e5 + 10;
ll f[N][3], s[N];
ll a[N];
int main() {
    ll n, x;
    re(n), re(x);
    ll ans = 0;
    for (int i = 1; i <= n; i++) {
        re(a[i]);
        f[i][0] = max(f[i - 1][0] + a[i], max(a[i], 0ll));
        f[i][1] = max(max(f[i - 1][0], f[i - 1][1]) + x * a[i], max(0ll, x * a[i]));
        f[i][2] = max(max({f[i - 1][2], f[i - 1][1], f[i - 1][0]}) + a[i], max(0ll, a[i]));
        ans = max({ans, f[i][0], f[i][1], f[i][2]});
    }
    printf("%lld\n", ans);
    return 0;
}

CF1446B. Catching Cheaters

*1800 LCS变形

  • LCS变形,将状态转移代价改为对答案的贡献
const int N = 5010;
int dp[N][N];
int main() {
    int n, m;
    IOS;
    cin >> n >> m;
    string a, b;
    cin >> a >> b;
    a = " " + a, b = " " + b;
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) - 1;
            dp[i][j] = max(0, dp[i][j]);
            if (a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 2);
            ans = max(ans, dp[i][j]);
        }
    }
    cout << ans << endl;
    return 0;
}

CF666A. Reberland Linguistics

*1800

已知有长度为\(n\)的由小写字母构成的一个字符串, 现在对字符串进行一些操作

  • 选择一个长度\(\color{red}\text{大于}\) \(4\)的“根”字符串
  • 然后在“根”字符串之后连接上任意个长度为2或3的“后缀”字符串。
    • 唯一的限制条件是,不可以连续接上两个相同的“后缀”字符串。

现在求构造过程中,有可能被用作“后缀”的字符串共有多少种。\(P.S:\)多个同样的“后缀”字符串只算一种。

思路

  • \(dp[i][j]\) i 开始接 j 个字符串是合法。
  • 从后往前做dp,转移条件是后面相同长度的串 不等且合法 或者 不同长度串合法
int dp[10010][4];
// dp[i][j] i 开始接 j 个字符串是合法。
int main() {
    IOS;
    string s;
    cin >> s;
    s = " " + s;
    int n = s.size() - 1;
    set<string> res;
    if (n <= 8) {
        if (n <= 6) {
            cout << 0 << endl;
            return 0;
        }
        else if (n == 7) {
            res.insert(s.substr(6));
        }
        else {
            res.insert(s.substr(6));
            res.insert(s.substr(7));
        }
        cout << SZ(res) << endl;
        for (auto t: res)
            cout << t << endl;
        return 0;
    }
    dp[n - 1][2] = 1, dp[n - 2][3] = 1;
    res.insert(s.substr(n - 1));
    res.insert(s.substr(n - 2));
    for (int i = n - 3; i >= 6; i--) {
        for (int j = 2; j <= 3; j++) {
            if (n - (i + j) + 1 < 2) continue;
            string a = s.substr(i, j), b = s.substr(i + j, j);
            if (dp[i + j][5 - j] || (a != b && dp[i + j][j])) dp[i][j] = 1;
            if (dp[i][j]) res.insert(a);
        }
    }
    cout << SZ(res) << endl;
    for (auto t: res)
        cout << t << endl;
    return 0;
}

CF682D. Alyona and Strings

*1900LCS变形

  • \(dp[i][j][k]\) 表示为 a 串前 i 位与 b 串前 j 位匹配 k 次的最大长度
  • 转移简单,但每次转移完需要更新为前缀最大值
int n, m, K;
const int N = 1010;
int dp[N][N][11];
char a[N], b[N];
int main() {
    re(n), re(m), re(K);
    scanf("%s", a + 1);
    scanf("%s", b + 1);
    int ans = 0;
    for (int k = 1; k <= K; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (a[i] == b[j]) {
                    dp[i][j][k] = max(dp[i - 1][j - 1][k] + 1, dp[i - 1][j - 1][k - 1] + 1);
                }
                ans = max(ans, dp[i][j][k]);
            }
        }
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++)
                dp[i][j][k] = max(dp[i][j][k], max(dp[i - 1][j][k], dp[i][j - 1][k]));
        }
    }
    printf("%d\n", ans);
    return 0;
}

CF1183H. Subsequences (hard version)

*1900

  • 设状态为 \(dp[i][j]\) 前 i 个字符,序列长度为 j 的方案数。
  • 转移:\(dp[i][j] = dp[i - 1][j] + (dp[i - 1][j - 1] - dp[pre[i] - 1][j - 1])\)
const int N = 1010;
ll dp[N][N], n, k, pos[26], pre[N];
char s[N];
int main() {
    re(n), re(k);
    scanf("%s", s + 1);
    for (int i = 1; i <= n; i++) {
        pre[i] = pos[(s[i] - 'a')];
        pos[(s[i] - 'a')] = i;
    }
    dp[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        dp[i][0] = 1;
        for (int j = 1; j <= i; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
            if (pre[i])
                dp[i][j] -= dp[pre[i] - 1][j - 1];
            // printf("dp[%d][%d]=%d\n", i,j,dp[i][j]);
        }
    }
    ll sum = 0;
    for (int j = n; j >= 0 && k > 0; j--) {
        ll cnt = dp[n][j];
        if (k >= cnt) {
            k -= cnt;
            sum += cnt * 1ll * (n - j);
        }
        else {
            sum += k * 1ll * (n - j);
            k = 0;
        }
    }
    if (k) puts("-1");
    else 
        printf("%lld\n", sum);
    return 0;
}
posted @ 2022-09-12 22:21  Roshin  阅读(42)  评论(0编辑  收藏  举报
-->