P16530 [THUPC 2026 决赛] 积木消除游戏 题解

前言

R圣模拟赛T4原题,虽然按难度排序是T1,考场上写了一个前缀和优化DP成功挂了70pts,赛后才发现前缀和写我只花了15min左右就AC了,但是由于神秘执念我还是写了一篇DP的题解,而且貌似是目前全网这题唯一DP题解。

题目大意

\(n\) 堆积木,第 \(i\) 堆数量为 \(a_i\)
每次可以选择一段长度至少为 \(k\) 的连续区间,并选择一个模数 \(m \in \{p,q\}\),将区间内所有积木数量对 \(m\) 取余。
可以进行任意多次操作,求最终所有积木总数的最小值。

性质

1:

对任意一堆积木,无论经过多少次取模操作,其最终值一定是下面两种之一:

  • \(a_i \bmod p\)
  • \((a_i \bmod q) \bmod p\)

因为先对 \(q\) 取模再对 \(p\) 取模,结果不会比直接对 \(p\) 取模更大,反之亦然。
因此每堆积木能达到的最小值是

\[\text{bst}_i = \min(a_i \bmod p,\ (a_i \bmod q) \bmod p) \]

2:

第一次选的整个区间都必须是同一种,剩下的随便选
第一次操作必然选择一段长度至少为 \(k\) 的连续区间,并对该区间内所有元素使用同一个模数。
在这段区间内,所有元素的最终值至少是它们对那个模数取模的结果。
区间外的元素可以通过后续操作调整到各自的 \(\text{bst}_i\)

由于任何偏离 \(\text{bst}_i\) 的取值都会使总和增加(增量非负),所以最优方案中:

  • 存在一段长度恰好为 \(k\) 的连续区间(长度更长只会让增量变大或不变);
  • 该区间内所有元素统一取 \(a_i \bmod p\) 或统一取 \((a_i \bmod q) \bmod p\)
  • 其余位置全部取 \(\text{bst}_i\)

3. 转化

\[d_1[i] = (a_i \bmod p) - \text{bst}_i \ge 0 \]

\[d_2[i] = ((a_i \bmod q) \bmod p) - \text{bst}_i \ge 0 \]

则答案

\[\text{ans} = \sum_{i=1}^n \text{bst}_i + \min\left( \min_{\text{长度 }k} \sum d_1,\ \min_{\text{长度 }k} \sum d_2 \right) \]

因为窗口内统一取模相较于取 \(\text{bst}_i\) 的增量就是对应的 \(d\) 之和。

实现

对每个 \(i\)

  • \(v_1 = a_i \bmod p\)
  • \(v_2 = (a_i \bmod q) \bmod p\)
  • \(\text{bst} = \min(v_1, v_2)\)
  • \(\text{sbt} += \text{bst}\)
  • \(d_1[i] = v_1 - \text{bst}\)
  • \(d_2[i] = v_2 - \text{bst}\)
    用 DP 维护长度为 \(k\) 的滑动窗口和:
  • \(\text{dp1}[i]\) 表示以 \(i\) 结尾、长度为 \(k\) 的窗口内 \(d_1\) 之和
  • \(\text{dp2}[i]\) 表示以 \(i\) 结尾、长度为 \(k\) 的窗口内 \(d_2\) 之和
  • 转移:\(\text{dp1}[i] = \text{dp1}[i-1] + d_1[i] - (i>k ? d_1[i-k] : 0)\)\(\text{dp2}\) 同理

取所有窗口中的最小值 \(\text{mn}_1, \text{mn}_2\)
答案 = \(\text{sbt} + \min(\text{mn}_1, \text{mn}_2)\)

正确性证明

懒得写了,和前缀和解法大差不差,可以看题解区orR圣的题解(在T4),但是注意到直接滑动窗口时间大爆炸,所以要前缀和\(O(n)\)预处理然后再跑。什么你问我前缀和优化DP我为什么不直接写前缀和,那你别管

复杂度分析

所有 \(n\) 之和不超过 \(10^5\),总复杂度 \(O(\sum n)\)

码(

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100005;
const ll INF = 1000000000000000000LL;
int n, k;
ll p, q;
ll a[N], d1[N], d2[N], dp1[N], dp2[N];
void solve()
{
    cin >> n >> k >> p >> q;
    ll sbt = 0;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        ll v1 = a[i] % p;
        ll v2 = (a[i] % q) % p;
        ll bst = v1;
        if (v2 < bst)
            bst = v2;
        sbt += bst;
        d1[i] = v1 - bst;
        d2[i] = v2 - bst;
    }
    ll mn1 = INF, mn2 = INF;
    dp1[0] = 0;
    dp2[0] = 0;
    for (int i = 1; i <= n; i++)
    {
        dp1[i] = dp1[i - 1] + d1[i];
        dp2[i] = dp2[i - 1] + d2[i];
        if (i > k)
        {
            dp1[i] -= d1[i - k];
            dp2[i] -= d2[i - k];
        }
        if (i >= k)
        {
            if (dp1[i] < mn1)
                mn1 = dp1[i];
            if (dp2[i] < mn2)
                mn2 = dp2[i];
        }
    }
    ll ans = sbt + mn1;
    if (mn2 < mn1)
        ans = sbt + mn2;
    cout << ans << '\n';
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int T;
    cin >> T;
    while (T--)
        solve();
    return 0;
}
posted @ 2026-09-23 20:12  想想ovo  阅读(1)  评论(0)    收藏  举报