Atcoder ABC456F Plan Holidays 题解

Atcoder ABC456F Plan Holidays 题解

Problem

给定 \(N\) 天,初始无假期,可执行两种操作:

  1. 花费 \(A_i\) 将第 \(i\) 天设为假期;
  2. 若第 \(i-1\)\(i+1\) 天已是假期,可免费将第 \(i\) 天设为假期。

求得到连续至少 \(K\) 天假期的最小总花费,共 \(T\) 组测试用例。

Thinking

朴素DP

1. 定义

固定左端点 \(L\),定义 \(dp[i]\) 表示:\(L\) 为连续假期的左端点,处理到第 \(i\) 天,且第 \(i\) 天手动付费的最小总花费

2. 转移

根据核心性质2,第 \(i\) 天付费时,前一个付费的天只有两种合法情况:

  • \(i-1\) 天也付费(无间隔,直接转移);
  • \(i-2\) 天付费(第 \(i-1\) 天可免费生成,无连续未付费)。

因此转移方程为:

\[dp[i] = \min(dp[i-1], dp[i-2]) + A[i] \]

3. 初始化

  • \(dp[L] = A[L]\):左端点必须手动付费,初始花费为 \(A[L]\)
  • \(dp[L+1] = A[L] + A[L+1]\):第 \(L+1\) 天付费时,只能从 \(L\) 转移(无 \(dp[L-1]\))。

4. 答案与复杂度

  • 答案:枚举所有左端点 \(L\),取所有满足 \(R \ge L+K-1\)\(dp[R]\) 的最小值。
  • 时间复杂度:\(O(N^2)\),枚举 \(L\)\(O(N)\),每个 \(L\) 递推 \(dp\)\(O(N)\),无法通过 \(N=2 \times 10^5\) 的数据。

矩阵优化

1. 什么是min-plus代数?

对普通矩阵乘法的运算做了替换,对应DP的「取最小花费+累加花费」逻辑:

普通代数 min-plus代数 对应DP逻辑
加法 \(+\) 取最小值 \(\min\) 选花费最小的方案
乘法 \(\times\) 加法 \(+\) 累加总花费
加法单位元 \(0\) 无穷大 \(\infty\) 表示不可能的方案
乘法单位元 \(1\) 数字 \(0\) 表示无额外花费

举个例子:普通代数的 \((a \times b) + (c \times d)\),在min-plus代数中就是 \(\min(a+b, c+d)\),正好是我们DP转移式的核心结构。

2. 状态向量定义

DP转移需要前两个状态的值,因此我们把递推所需的信息打包成行向量,对应代码中的vec结构体:

  • 旧状态(处理到第 \(i-1\) 天):\(vec_{old} = \begin{bmatrix} dp[i-1] & dp[i-2] \end{bmatrix}\)
  • 新状态(处理到第 \(i\) 天):\(vec_{new} = \begin{bmatrix} dp[i] & dp[i-1] \end{bmatrix}\)

其中vec.a1对应\(dp[i]\)vec.a2对应\(dp[i-1]\),与代码完全一致。

3. 转移矩阵的完整推导(一步不跳,完全对应代码)

我们的目标是找到2×2矩阵 \(M_i\),使得 \(vec_{new} = vec_{old} \times M_i\),其中\(\times\)为min-plus矩阵乘法。

首先明确行向量×矩阵的min-plus运算规则(完全对应代码add_tag函数):
对于矩阵 \(M = \begin{bmatrix} M.a11 & M.a12 \\ M.a21 & M.a22 \end{bmatrix}\),行向量 \(vec_{old} = \begin{bmatrix} v1 & v2 \end{bmatrix}\) 与之相乘的结果为:

\[\begin{align*} new\_v1 &= \min(v1 + M.a11,\ v2 + M.a21) \\ new\_v2 &= \min(v1 + M.a12,\ v2 + M.a22) \end{align*} \]

接下来我们结合DP转移式,解出矩阵的4个元素:

① 解第一列(对应\(new\_v1 = dp[i]\)

我们需要 \(new\_v1 = dp[i] = \min(dp[i-1], dp[i-2]) + A[i]\),拆分为min-plus形式:

\[dp[i] = \min(dp[i-1] + A[i],\ dp[i-2] + A[i]) \]

与运算规则对比,直接得到:

\[M.a11 = A[i],\quad M.a21 = A[i] \]

② 解第二列(对应\(new\_v2 = dp[i-1]\)

我们需要 \(new\_v2 = dp[i-1]\),即新向量的第二个元素等于旧向量的第一个元素\(v1\)。根据min的性质\(\min(a, \infty)=a\),只需满足:

\[v1 + M.a12 = v1 \implies M.a12 = 0 \]

\[v2 + M.a22 = \infty \implies M.a22 = \infty \]

③ 最终转移矩阵

\[M_i = \begin{bmatrix} A[i] & 0 \\ A[i] & \infty \end{bmatrix} \]

完全对应代码中的mat M(a[i], 0, a[i], inf)

4. 样例验证

以样例1的\(i=4\)为例,\(A[4]=1\),旧状态\(vec_{old} = \begin{bmatrix} 5 & 1 \end{bmatrix}\)\(dp[3]=5, dp[2]=1\)):

\[\begin{align*} new\_v1 &= \min(5+1, 1+1) = 2 \\ new\_v2 &= \min(5+0, 1+\infty) = 5 \end{align*} \]

得到\(vec_{new} = \begin{bmatrix} 2 & 5 \end{bmatrix}\),其中\(dp[4]=2\)正是样例的最优解,完全正确。

Solution

1. 枚举顺序转换

朴素DP是固定左端点\(L\)递推右端点\(R\),复杂度\(O(N^2)\)。我们反过来枚举右端点\(i\),维护所有左端点\(L\)的状态,实现批量处理:

  1. \(i\)增加1时,所有已初始化的左端点\(L < i\),都可以通过乘以\(M_i\)批量转移到第\(i\)天的状态;
  2. \(i=L\)时,单点初始化左端点\(L\)的状态为\(vec = \begin{bmatrix} A[L] & \infty \end{bmatrix}\)
  3. \(i \ge K\)时,查询所有\(L \le i-K+1\)\(dp[i]\)的最小值(满足连续长度≥K),更新全局答案。

2. 线段树设计

上述三个操作正好可以用线段树高效实现,每个操作时间复杂度\(O(\log N)\)

  • 区间更新:给\([1, i-1]\)的所有状态批量应用转移矩阵\(M_i\),用矩阵作为懒标记;
  • 单点更新:给位置\(i\)初始化状态向量;
  • 区间查询:查询\([1, i-K+1]\)\(vec.a1\)的最小值。

线段树细节

  • 节点维护:每个节点维护区间内所有状态的\(\min(vec.a1)\)\(\min(vec.a2)\),可直接由左右子节点合并(对应pushup函数);
  • 懒标记:用矩阵作为懒标记,矩阵乘法满足结合律,多个转移矩阵可合并(对应mul函数);
  • 单位矩阵:懒标记初始值为单位矩阵,乘以单位矩阵不改变状态(对应is_unit函数,判断是否需要下传懒标记)。

Code

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll inf = 1e18;
const int N = 2e5 + 10;
ll a[N];
struct mat {
    ll a11, a12, a21, a22;
    mat(ll a11 = 0, ll a12 = inf, ll a21 = inf, ll a22 = 0)
        : a11(a11), a12(a12), a21(a21), a22(a22) {}
    bool is_unit() const {
        return a11 == 0 && a12 >= inf / 2 && a21 >= inf / 2 && a22 == 0;
    }
};
struct vec {
    ll a1, a2;
    vec(ll a1 = inf, ll a2 = inf) : a1(a1), a2(a2) {}
};
vec tr[N << 2]; mat tag[N << 2];
vec add_tag(const vec &v, const mat &m) {
    return vec(
        min(v.a1 + m.a11, v.a2 + m.a21),
        min(v.a1 + m.a12, v.a2 + m.a22)
    );
}
mat mul(const mat &a, const mat &b) {
    return mat(
        min(a.a11 + b.a11, a.a12 + b.a21),
        min(a.a11 + b.a12, a.a12 + b.a22),
        min(a.a21 + b.a11, a.a22 + b.a21),
        min(a.a21 + b.a12, a.a22 + b.a22)
    );
}
void put_tag(int p, const mat &m) {
    tr[p] = add_tag(tr[p], m);
    tag[p] = mul(tag[p], m);
}
void pushdown(int p) {
    if (!tag[p].is_unit()) {
        put_tag(p << 1, tag[p]);
        put_tag(p << 1 | 1, tag[p]);
        tag[p] = mat();
    }
}
void pushup(int p) {
    tr[p].a1 = min(tr[p << 1].a1, tr[p << 1 | 1].a1);
    tr[p].a2 = min(tr[p << 1].a2, tr[p << 1 | 1].a2);
}
void build(int p, int l, int r) {
    tag[p] = mat();
    if (l == r) return tr[p] = vec(inf, inf), void();
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    pushup(p);
}
void updr(int p, int l, int r, int ql, int qr, const mat &m) {
    if (ql <= l && r <= qr) return put_tag(p, m), void();
    pushdown(p);
    int mid = (l + r) >> 1;
    if (ql <= mid) updr(p << 1, l, mid, ql, qr, m);
    if (qr > mid)  updr(p << 1 | 1, mid + 1, r, ql, qr, m);
    pushup(p);
}
void updp(int p, int l, int r, int idx, const vec &v) {
    if (l == r) return tr[p] = v, void();
    pushdown(p);
    int mid = (l + r) >> 1;
    if (idx <= mid) updp(p << 1, l, mid, idx, v);
    else updp(p << 1 | 1, mid + 1, r, idx, v);
    pushup(p);
}
ll query(int p, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) return tr[p].a1;
    pushdown(p);
    int mid = (l + r) >> 1;
    ll res = inf;
    if (ql <= mid) res = min(res, query(p << 1, l, mid, ql, qr));
    if (qr > mid)  res = min(res, query(p << 1 | 1, mid + 1, r, ql, qr));
    return res;
}
void solve() {
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++) cin >> a[i];
    build(1, 1, n);
    ll ans = inf;
    for (int i = 1; i <= n; i++) {
        mat M(a[i], 0, a[i], inf);
        if (i > 1) updr(1, 1, n, 1, i - 1, M);
        updp(1, 1, n, i, vec(a[i], inf));
        if (i >= k) {
            int L = i - k + 1;
            ans = min(ans, query(1, 1, n, 1, L));
        }
    }
    cout << ans << '\n';
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t; cin >> t;
    while (t--) solve();
    return 0;
}
posted @ 2026-05-02 22:58  Aojun  阅读(43)  评论(0)    收藏  举报