跳转至

动态规划练习

2025-04-07

最近准备练点动态规划的题。。。但是其实好像只是在洛谷写了几个绿色的板子题。基本都是很简单的难度。

感觉也是可以稍微记录一下吧,但是后面得稍微写点难点的题了,虽然有的绿也是感觉没啥思路。

稍微总结一下。感觉都还是比较典的题,挺好的,补起来应该也不会很困难。

洛谷P2679 普及+/提高

感觉好像没啥好说的,枚举 \(A_i\)\(B_j\) 匹配,匹配了多少段。\(i\) 这一维只需要知道前一位的情况,可以用滚动数组。

\(dp_{i,j,l}\) 表示 \(A_{i}\)\(B_j\) 匹配用了 \(l\) 段的方案数。

转移方程为 \(dp_{i,j,l}=\sum\limits_{k=1}^{i-1}{dp_{k,j-1,l-1}}+dp_{i-1,j-1,l}\)

所以我们其实只需要知道前缀的和,以及 \(dp_{i-1}\) ,所以可以使用滚动数组,再用一个数组来记前缀和。

constexpr int mod = 1000000007;

int dp[2][202][202];
void ChatGptDeepSeek()
{
    int n, m, k;
    cin >> n >> m >> k;
    vector<char> A(n + 1), B(m + 1);
    for (int i = 1; i <= n; i++)
        cin >> A[i];
    for (int i = 1; i <= m; i++)
        cin >> B[i];
    vector<vector<int>> DP(m + 1, vector<int>(k + 1));
    dp[0][0][0] = DP[0][0] = 1;
    for (int i = 1; i <= n; i++)
    {

        vector nDP = DP;
        for (int j = 1; j <= min(i, m); j++)
        {
            if (A[i] == B[j])
            {
                for (int l = 1; l <= min(k, j); l++)
                {
                    dp[i & 1][j][l] = (dp[i & 1][j][l] + DP[j - 1][l - 1]) % mod;
                    nDP[j][l] = (nDP[j][l] + DP[j - 1][l - 1]) % mod;

                    dp[i & 1][j][l] = (dp[i & 1][j][l] + dp[(i ^ 1) & 1][j - 1][l]) % mod;
                    nDP[j][l] = (nDP[j][l] + dp[(i ^ 1) & 1][j - 1][l]) % mod;
                }
            }
        }
        for (int j = 0; j <= min(i, m); j++)
            for (int l = 1; l <= min(k, j); l++)
                dp[(i ^ 1) & 1][j][l] = 0;
        DP = nDP;
    }
    cout << DP[m][k] << '\n';
}

洛谷P1541 普及+/提高

这个很基础的题目了,虽然标的有绿,但是实际难度并非很高,应该还是比较容易想出来的。只需要枚举每种类型类型的卡片用了多少次。

因为走的步数是可以根据用的卡片的情况直接算出的。

int dp[41][41][41][41];

void ChatGptDeepSeek()
{
    int n, m;
    cin >> n >> m;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    vector<int> b(4);
    for (int i = 1; i <= m; i++)
    {
        int x;
        cin >> x;
        b[x - 1]++;
    }
    dp[0][0][0][0] = a[1];
    for (int i = 0; i <= b[0]; i++)
        for (int j = 0; j <= b[1]; j++)
            for (int k = 0; k <= b[2]; k++)
                for (int l = 0; l <= b[3]; l++)
                {
                    int len = i * 1 + j * 2 + k * 3 + l * 4;
                    // dp[i][j][k][l] = a[1];
                    // cerr << len << '\n';
                    if (i - 1 >= 0)
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i - 1][j][k][l] + a[len + 1]);
                    if (j - 1 >= 0)
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i][j - 1][k][l] + a[len + 1]);
                    if (k - 1 >= 0)
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i][j][k - 1][l] + a[len + 1]);
                    if (l - 1 >= 0)
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i][j][k][l - 1] + a[len + 1]);
                }
    cout << dp[b[0]][b[1]][b[2]][b[3]] << '\n';
}

洛谷P1868 普及+/提高

也是比较容易想到的了。定义 \(dp_i\)\([1,i]\) 的最大答案。

那么对于每一个区间 \([l,r]\) ,我们可以更新 \(dp_r=dp_{l-1}+r-l+1\)

constexpr int N = 3e6 + 10;
int dp[N];

void ChatGptDeepSeek()
{
    int n;
    cin >> n;
    vector<pair<int, int>> v(n);
    for (int i = 0; i < n; i++)
        cin >> v[i].first >> v[i].second;
    sort(v.begin(), v.end());
    int j = 0;
    for (int i = 0; i <= 3000000; i++)
    {
        while (j < v.size() && v[j].first == i)
        {
            auto [l, r] = v[j];
            j++;
            dp[r] = max(dp[r], (l ? dp[l - 1] : 0) + r - l + 1);
        }
        dp[i] = max(dp[i], dp[i - 1]);
    }
    cout << dp[3000000] << '\n';
}

洛谷P1156 普及+/提高

也是比较常规的题目。维护每一个高度时的最大的体力值。

只需要注意下,输入的时间不全是按时间排序的。

void ChatGptDeepSeek()
{
    int D, G;
    cin >> D >> G;
    vector<int> T(G), F(G), H(G);
    {
        vector<array<int, 3>> tmp(G);
        for (int i = 0; i < G; i++)
            cin >> tmp[i][0] >> tmp[i][1] >> tmp[i][2];
        sort(tmp.begin(), tmp.end());
        for (int i = 0; i < G; i++)
            T[i] = tmp[i][0], F[i] = tmp[i][1], H[i] = tmp[i][2];
    }
    // for (int i = 0; i < G; i++)
    //     cin >> T[i] >> F[i] >> H[i];
    dp[1][0] = 10;
    // for (int i = 1; i <= 25000; i++)
    //     dp[1][i] = dp[0][i] = -1;

    for (int i = 0; i < G; i++)
    {
        for (int j = 0; j <= 25000; j++)
        {

            if (dp[(i & 1) ^ 1][j] >= T[i])
            {
                if (j + H[i] >= D)
                {
                    // cerr << j << " " << dp[(i & 1) ^ 1][j] << " \n";
                    cout << T[i] << '\n';
                    return;
                }
                if (j + H[i] <= 25000)
                    dp[(i & 1)][j + H[i]] = max(dp[(i & 1)][j + H[i]], dp[(i & 1) ^ 1][j]);
                dp[(i & 1)][j] = max(dp[(i & 1)][j], dp[(i & 1) ^ 1][j] + F[i]);
                // cerr << i << " " << j << '\n';
            }
        }
        // for (int j = 1; j <= 25000; j++)
        //     dp[(i & 1) ^ 1][j] = -1;
    }
    // 如果不行的话 就一直吃!
    int now = 10;
    for (int i = 0; i < G; i++)
    {
        // cerr << now << " " << T[i] << '\n';
        if (now < T[i])
            break;
        now += F[i];
    }
    cout << now << '\n';
}

洛谷P3558 普及+/提高

其实也不是很难,只需要稍微思考一下。虽然说 WA 了几次,刚开始题目没看清。。。然后思路也是有问题的。后来稍微改一下就好了。神奇的是,这五题居然都不用看题解。

枚举每一位变成 \(-1,0,1\) 的前缀的答案,\(a_i\) 最多会被前面的数字加两次,且加了之后一定要大于等于前面的数字。

void ChatGptDeepSeek() // Date: 2025-04-07
{                      // Time: 17:51:48
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    // 枚举每一位变成 -1 0 1 的答案
    vector<vector<int>> dp(n + 1, vector<int>(3, 1e9));
    // ... 好像想复杂了的
    dp[1][a[1] + 1] = 0;
    for (int i = 2; i <= n; i++)
    {
        for (int x = -1; x <= 1; x++)
        {
            for (int z = 0; z <= 2; z++)
            {
                int y = a[i] + z * x;
                if (y < x || y > 1)
                    continue;
                dp[i][y + 1] = min(dp[i][y + 1], dp[i - 1][x + 1] + z);
            }
        }
    }
    int ans = min({dp[n][0], dp[n][1], dp[n][2]});
    if (ans == 1e9)
        cout << "BRAK\n";
    else
        cout << ans << '\n';
}

洛谷P5322 普及+/提高

又一道水题,直接枚举就行吧。复杂度 \(O(nms)\)

枚举派了 \(k\) 个人的最大得分。

int dp[2][20002];

void ChatGptDeepSeek() // Date: 2025-04-07
{                      // Time: 19:29:06
    int s, n, m;
    cin >> s >> n >> m;
    vector<vector<int>> a(n + 1, vector<int>(s + 1));
    for (int i = 1; i <= s; i++)
    {
        for (int j = 1; j <= n; j++)
            cin >> a[j][i];
    }

    for (int i = 1; i <= n; i++)
    {
        sort(a[i].begin() + 1, a[i].end());
        for (int j = 1; j <= s; j++)
        {
            int need = 2 * a[i][j] + 1;
            if (need > m)
                break;
            for (int k = m; k >= need; k--)
            {
                dp[i & 1][k] = max(dp[i & 1][k], dp[i & 1 ^ 1][k - need] + i * j);
                // cerr << dp[k] << " ";
            }
            // cerr << '\n';
        }
        for (int k = 0; k <= m; k++)
            dp[i & 1 ^ 1][k] = dp[i & 1][k];
        // cout << *max_element(dp + 1, dp + m + 1) << '\n';
    }
    int ans = 0;
    for (int i = 0; i <= m; i++)
        ans = max(ans, dp[n & 1][i]);
    cout << ans << '\n';
}

洛谷P1880 普及+/提高

石子合并,应该是区间 DP 这一块的模板题了。也是第一次自己 AC 出的绿色的区间 DP 题。

但是刚开始看到是可能没啥思路。。或者说没怎么想。于是去搜了下区间 DP ,结果看到一个 石子合并[弱化版] ,于是花了半小时 AC 了它,于是再看这题发现只是变成了环形的情况,数据范围还变小了。于是花了 40 分钟 AC 了这题。

其实跟弱化版其实没区别,其实不用怎么特殊处理环形的。就是区间 DP 模板的,如果不太会,可以先学习一下再来 AC 。

ll dp[2][114][114];

void ChatGptDeepSeek() // Date: 2025-04-07
{                      // Time: 20:35:26
    int n;
    cin >> n;
    vector<int> a(n + 1), pre(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }
    auto get = [&](int l, int r)
    {
        if (l <= r)
            return pre[r] - pre[l - 1];
        else
            return pre[n] - pre[l - 1] + pre[r];
    };
    { // 先把最小值求了吧
        for (int i = 0; i < 114; i++)
            for (int j = 0; j < 114; j++)
                dp[0][i][j] = 1e9;
        for (int i = 1; i <= n; i++)
            dp[0][i][i] = 0;
        for (int len = 2; len <= n; len++)
        {
            for (int l = 1; l <= n; l++)
            {
                int r = l + len - 1;
                if (r > n)
                    r -= n;
                for (int k = l; k != r; k++)
                {
                    if (k > n)
                    {
                        k = 0;
                        continue;
                    }
                    dp[0][l][r] = min(dp[0][l][r], dp[0][l][k] + dp[0][(k + 1 > n ? k + 1 - n : k + 1)][r] + get(l, r));
                    dp[1][l][r] = max(dp[1][l][r], dp[1][l][k] + dp[1][(k + 1 > n ? k + 1 - n : k + 1)][r] + get(l, r));
                }
            }
        }
        ll ans[]{1000000000, 0};
        for (int l = 1; l <= n; l++)
        {
            int r = l + n - 1;
            if (r > n)
                r -= n;
            ans[0] = min(ans[0], dp[0][l][r]), ans[1] = max(ans[1], dp[1][l][r]);
        }
        cout << ans[0] << '\n'
             << ans[1] << '\n';
    }
}

洛谷P1775 普及/提高-

双倍经验了属于是。。虽然这题只是黄难度,但如果之前是没见过区间 DP 的话,应该会比较难想出来。比如我。只在前不久的一个 Div. 3 的 G 题见过区间 DP ,约等于抄的题解,还抄了一个蓝题的题解,调色那个。所以这次我自己想出来了。因为就是模板啊。。。

ll dp[303][303];

void ChatGptDeepSeek() // Date: 2025-04-07
{                      // Time: 20:41:23
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 0; i <= 302; i++)
        for (int j = 0; j <= 302; j++)
            dp[i][j] = 1e9;
    for (int i = 1; i <= n; i++)
        cin >> a[i], dp[i][i] = 0;
    vector<int> pre(n + 1);
    for (int i = 1; i <= n; i++)
        pre[i] = pre[i - 1] + a[i];
    for (int len = 2; len <= n; len++)
    {
        for (int l = 1; l + len - 1 <= n; l++)
        {
            int r = l + len - 1;
            for (int k = l; k < r; k++)
                dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + pre[r] - pre[l - 1]);
        }
    }
    cout << dp[1][n] << '\n';
}

洛谷P3146 普及+/提高

想笑,怎么五分钟不到AC了,虽然代码确实很短。那就不多说了,也是区间DP板子题。

int dp[250][250];

void ChatGptDeepSeek() // Date: 2025-04-08
{                      // Time: 15:04:31
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++)
        cin >> a[i], dp[i][i] = a[i];
    for (int len = 2; len <= n; len++)
    {
        for (int l = 1; l + len - 1 <= n; l++)
        {
            int r = l + len - 1;
            for (int k = l; k < r; k++)
            {
                if (dp[l][k] > 0 && dp[k + 1][r] > 0 && dp[l][k] == dp[k + 1][r])
                    dp[l][r] = max(dp[l][r], dp[l][k] + 1);
            }
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) ans=max(ans,dp[i][j]);
    cout<<ans<<'\n';
}

洛谷P1063 普及+/提高

maodie1

这题是比较有难度的,因为我们合并的时候是要看后面一个的后一个是什么。这是这个页面到这里第一次看题解。。

\(dp_{l,r}=max(dp_{l,r},dp_{l,k}+dp_{k,r}+a_la_ka_r)\)

因为它这个也是一个环形的,所以最后的答案会是 \(dp_{i,i+n}\) ,这个还是需要一些理解的...

int dp[202][202];
void ChatGptDeepSeek() // Date: 2025-04-08
{                      // Time: 16:03:12
    int n;
    cin >> n;
    vector<int> a(2 * n + 1);
    for (int i = 1; i <= n; i++)
        cin >> a[i], a[i + n] = a[i];
    for (int len = 2; len <= n + 1; len++)
        for (int l = 1; l + len - 1 <= 2 * n; l++)
        {
            int r = l + len - 1;
            for (int k = l + 1; k < r; k++)
            {
                dp[l][r] = max(dp[l][r], dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
                // cerr << l << " " << r << " " << dp[l][r] << '\n';
            }
        }
    int ans = 0;
    for (int i = 1; i <= n; i++)
        ans = max(ans, dp[i][i + n]);
    cout << ans << '\n';
}

洛谷P1040 普及+/提高

也是不难的,但看了一下题解的,这题不该看的。看下先序中序的概念就行的。。

先序遍历就是先遍历根节点,再左右节点;中序遍历就是先遍历左子树,再遍历根节点;后续遍历就是先遍历左右子树,再遍历根节点。所以先中后指的都是遍历根节点的顺序。

这题还是比较基础的,算每个 \([l,r]\) 子树的答案。因为最后要输出先序遍历,所以我们再拿个数组记录答案。

void ChatGptDeepSeek() // Date: 2025-04-11
{                      // Time: 10:13:41
    int n;
    cin >> n;
    vector<int> d(n + 1);

    vector<vi> dp(n + 1, vi(n + 1, 1));
    vector<vi> root(n + 1, vi(n + 1));
    for (int i = 1; i <= n; i++)
        cin >> d[i], dp[i][i] = d[i], root[i][i] = i;
    for (int len = 2; len <= n; len++)
    {
        for (int l = 1; l + len - 1 <= n; l++)
        {
            int r = l + len - 1;

            for (int k = l; k < r; k++)
            {
                if (dp[l][r] < dp[l][k - 1] * dp[k + 1][r] + dp[k][k])
                {
                    dp[l][r] = dp[l][k - 1] * dp[k + 1][r] + dp[k][k];
                    root[l][r] = k;
                }
            }
        }
    }
    auto Print = [&](auto &&self, int l, int r) -> void
    {
        if (l > r)
            return;
        cout << root[l][r] << ' ';
        if (l == r)
            return;
        self(self, l, root[l][r] - 1);
        self(self, root[l][r] + 1, r);
    };
    cout << dp[1][n] << '\n';
    Print(Print, 1, n);
}

洛谷P1273 普及+/提高

本来想的是 \(dp_{u,i}\) 代表 \(u\) 节点赚了 \(i\) 金币的最大人数,但是复杂度会是 \(n^3\) ,而且也不太好写。

其实有时一种思路想不通,可以稍微想想别的,像这种并非很复杂的 DP,总共也不会有很多可能性的。

于是想到可以代表的是 \(u\) 节点的子树有 \(i\) 人的最小花费,然后 TLE 了几个,加了 break 之后就好了,因为每个子树的大小都是有限的。

这是我的第99道绿题,今天再写一题就。。等会杭电比赛,明天蓝桥省赛。

void ChatGptDeepSeek() // Date: 2025-04-11
{                      // Time: 11:04:40
    int n, m;
    cin >> n >> m;
    vector<vector<pii>> adj(n + 1, vector<pii>());
    for (int i = 1; i <= n - m; i++)
    {
        int k;
        cin >> k;
        for (int j = 1; j <= k; j++)
        {
            int v, w;
            cin >> v >> w;
            adj[i].push_back({v, w});
        }
    }

    vector<vi> dp(n + 1, vi(m + 1, -INF));
    for (int i = 1; i <= n; i++)
        dp[i][0] = 0;
    for (int i = n - m + 1; i <= n; i++)
    {
        int x;
        cin >> x;
        dp[i][1] = x;
    }
    vector<vi> ndp(dp);

    auto dfs = [&](auto &&self, int u, int pre) -> void
    {
        // cerr << u << '\n';
        ndp[u] = dp[u];
        for (auto [v, w] : adj[u])
        {
            if (v == pre)
                continue;
            self(self, v, u);
            // 知道子结点的cost情况 能怎么样呢?
            // 改一下 dp[i,j]表示节点i 有j 的贡献 的最小的cost
            for (int i = 0; i <= m; i++)
            {
                if (dp[u][i] == -INF)
                    break;
                for (int j = 0; j + i <= m; j++)
                {
                    if (dp[v][j] == -INF)
                        break;
                    ndp[u][i + j] = max(ndp[u][i + j], dp[u][i] + dp[v][j] - w);
                }
            }
            dp[u] = ndp[u];
        }
    };
    dfs(dfs, 1, 0);
    for (int i = m; i >= 0; i--)
    {
        if (dp[1][i] >= 0)
        {
            cout << i << '\n';
            return;
        }
    }
}