1 条题解
-
1
题意简述
给定序列 ,每个位置 可以花费 将 修改为任意非负整数。求最小总代价,使得修改后所有前缀和 都不是 的倍数。
关键观察
观察 1:修改 = 重置前缀和。 修改位置 后,前缀和 可以变成任意一个非零余数(因为 可以改成任意值, 的余数可自由选择)。而 之后的位置若不再修改,其前缀和等于 加上确定的偏移量。
观察 2:段分割。 我们把被修改的位置称为"修改点"。相邻两个修改点(以及起点 与第一个修改点、最后一个修改点与终点 )之间是一段"不修改"的连续区间。段内所有位置的前缀和,等于段起点的前缀和加上段内的连续和。
观察 3:段可行的充要条件。 考虑从修改点 出发、到下一个修改点 结束(不修改 )的一段。段起点前缀和 可自由取任意非零值,需避开使得段内某前缀和变为 的那些取值。
设初始前缀和 。段内位置 的前缀和(修改 后整体加 )为 ,要求不为 ,即 。
有 个非零选择,被禁止的选择对应 的不同取值。因此:
段可行 ⟺ 区间 内不同前缀和 的数量 。
证明:区间内不同 有 个,则禁止的 至多 个(其中 对应 ,本就非零选择之外)。需要存在非零 未被禁止,即 ,等价于 。
观察 4:起点段特殊。 起点 处 是固定的(不能自由选择),因此第一段(从 到第一个修改点)要求 都非零,这是确定的判断。
算法
设 表示"位置 是修改点"的最小代价( 表示起点)。
转移:
$$dp[i] = c_i + \min\left(\underbrace{[S_1\dots S_{i-1}\text{ 都非零}]\cdot dp[0]}_{\text{起点段}},\ \min_{\substack{1\le j<i \\ [j,i-1]\text{ 不同 }S<r}} dp[j]\right)$$最终答案为:
$$\min\left([S_1\dots S_n\text{ 都非零}]\cdot 0,\ \min_{\substack{1\le j\le n \\ [j,n]\text{ 不同 }S<r}} dp[j]\right)$$优化
-
时:任意区间的不同前缀和数量 区间长度 ,故所有段都可行。此时只需在第一个"坏点"()之前选择一个最小 修改一次,答案为 ,其中 是第一个 的位置(若不存在则为 )。
-
时:用双指针 + 单调队列做到 。固定 ,随 减小,区间 变长,不同前缀和数量单调不减,故可行 是连续区间 。用双指针维护 (窗口 内不同 数量 ),用单调队列维护窗口内 的最小值。
复杂度
- 时间复杂度: ( 时双指针+单调队列; 时直接扫描),总复杂度 。
- 空间复杂度: 。
参考代码
#include <bits/stdc++.h> using namespace std; typedef long long ll; const ll INF = 4e18; void solve() { int n; ll r; scanf("%d%lld", &n, &r); vector<ll> a(n + 1), c(n + 1); for (int i = 1; i <= n; i++) scanf("%lld", &a[i]); for (int i = 1; i <= n; i++) scanf("%lld", &c[i]); vector<ll> s(n + 1); s[0] = 0; for (int i = 1; i <= n; i++) s[i] = (s[i - 1] + a[i]) % r; vector<char> ok(n + 1); ok[0] = 1; int i0 = -1; for (int i = 1; i <= n; i++) { ok[i] = ok[i - 1] && (s[i] != 0); if (s[i] == 0 && i0 == -1) i0 = i; } if (ok[n]) { printf("0\n"); return; } if (r > (ll)n) { ll ans = INF; for (int i = 1; i <= i0; i++) ans = min(ans, c[i]); printf("%lld\n", ans); return; } int R = (int)r; vector<int> freq(R, 0); int cnt = 0, L = 1; vector<ll> dp(n + 1, INF); dp[0] = 0; deque<int> q; for (int i = 1; i <= n; i++) { while (!q.empty() && q.front() < L) q.pop_front(); ll best = INF; if (ok[i - 1]) best = 0; if (!q.empty()) best = min(best, dp[q.front()]); dp[i] = c[i] + best; if (freq[s[i]] == 0) cnt++; freq[s[i]]++; while (cnt >= R && L <= i) { freq[s[L]]--; if (freq[s[L]] == 0) cnt--; L++; } while (!q.empty() && dp[q.back()] >= dp[i]) q.pop_back(); q.push_back(i); } ll ans = INF; while (!q.empty() && q.front() < L) q.pop_front(); if (!q.empty()) ans = min(ans, dp[q.front()]); printf("%lld\n", ans); } int main() { int T; scanf("%d", &T); while (T--) solve(); return 0; }另一种思路(供参考)
也可以做 的朴素 DP:设 表示当前前缀和余数为 的最小代价,每次要么"不修改"(余数唯一转移 ,且结果不能为 ),要么"修改"(可达任意非零余数,代价为 )。这在 时可通过子任务 2。
-
- 1
信息
- ID
- 82
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 8
- 标签
- 递交数
- 28
- 已通过
- 5
- 上传者