【题解】「KDOI-06-S」补题
「KDOI-06-S」
A.「KDOI-06-S」消除序列
赛时写了一个 \(O(nq)\) 的线性 DP,喜提 60 分。
线段树 / ST 表 (\(O(n + \sum m \log n)\) / \(O(n log n + \sum m)\), teached by @westernhan)
注意到如果操作 1 被使用,则一定只会使用一次,而且在最优策略中一定是第一次使用操作 1。则我们可以通过以下方式进行操作,使序列满足条件:
首先执行 \(a_i\) 和 \(\sum^{j \le i,i \in P}_{j = 1} c_j\) 使前 \(i\) 个元素符合条件,然后执行 \(\sum_{j = i + 1}^{j \le n, j \not \in P} b_i\) 使 \([i+1, n]\) 符合条件。
考虑如何计算:\(\sum_{j = i + 1}^{j \le n, j \not \in P} b_j = \sum_{j = i + 1}^{j \le n} b_i - \sum_{j = i + 1}^{j \le n, j \in P} b_j\) ,两项都可以后缀和维护,则原式 \(=\)
\[(a_i + \sum^{j \le i,i \in P}_{j = 1} c_j) + (\sum_{j = i + 1}^{j \le n} b_j - \sum_{j = i + 1}^{j \le n, j \in P} b_j) \\ = (a_i + \sum_{j = i + 1}^{j \le n} b_j) + (\sum^{j \le i,i \in P}_{j = 1} c_j - \sum_{j = i + 1}^{j \le n, j \in P} b_j)
\]
后两项对于同一段 \([p_i,p_{i+1})\) 相同,前两项最小值可以 ST 表或者线段树维护。所以可以枚举每一段,每次做到每次询问复杂度为 \(O(m)\) 或者 \(O(m \log n)\),足以通过本题(喜闻乐见地,上次洛谷网校 CSP-S 模拟赛的 T1 把线段树卡到了 60 分,而这次没卡)。
容易证明最优解的操作一定符合这种方式,因为这种方式没有重复操作。
参考代码(code by @westernhan):
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll inf = 1e18;
int n, m, q, a[500005], b[500005], c[500005], p[500005];
ll sumb[500005], sumc[500005], num[500005];
class ST
{
public:
ll t[500005][35];
ll logg(ll w)
{
ll res = -1;
while (w)
w >>= 1, res++;
return res;
}
void init(void)
{
for (int i = 0; i <= n; i++)
t[i][0] = sumb[i + 1] + a[i];
for (int i = 1; i <= 25; i++)
for (int j = 0; j + (1 << (i - 1)) <= n; j++)
t[j][i] = min(t[j][i - 1], t[j + (1 << (i - 1))][i - 1]);
}
ll query(int l, int r)
{
if (l > r)
return inf;
int len = logg(r - l + 1);
return min(t[l][len], t[r - (1 << len) + 1][len]);
}
} T;
int main(void)
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", a + i); // copy 0
for (int i = 1; i <= n; i++)
scanf("%d", b + i); // set 0
for (int i = n; i >= 1; i--)
sumb[i] = sumb[i + 1] + b[i];
for (int i = 1; i <= n; i++)
scanf("%d", c + i); // set 1
T.init();
scanf("%d", &q);
while (q--)
{
ll maxn = inf;
scanf("%d", &m);
for (int i = 1; i <= m; i++)
{
scanf("%d", p + i);
sumc[i] = sumc[i - 1] + c[p[i]];
num[i] = num[i - 1] + b[p[i]];
}
for (int i = 1; i <= m; i++)
{
ll res = 0;
res += T.query(p[i - 1], p[i] - 1);
res -= num[m] - num[i - 1];
res += sumc[i - 1];
maxn = min(maxn, res);
}
ll res = 0;
res += T.query(p[m], n);
res += sumc[m];
maxn = min(res, maxn);
printf("%lld\n", maxn);
}
return 0;
}
官方题解给了一种线性做法:设 \(f_i\) 为 \(a_i + \sum_{j \in (i, n]} b_i\) 的前缀最小值,用一个变量维护后两项的取值(我也有点没弄懂,求解释)。
参考代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n;
int a[N], b[N], c[N];
int q;
int m, p[N];
using ll = long long;
ll suf_b[N], pre_min[N];
const ll inf = 0x66ccffLL * 'l' * 't' * 'y' * '5' * '2' * '0';
signed main()
{
#ifdef DEBUG
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
scanf("%d\n", &n);
for (int i = 1; i <= n; i++)
scanf("%d ", &a[i]);
for (int i = 1; i <= n; i++)
scanf("%d ", &b[i]);
for (int i = 1; i <= n; i++)
scanf("%d ", &c[i]);
for (int i = n; i >= 1; i--)
suf_b[i] = suf_b[i + 1] + b[i];
pre_min[0] = suf_b[1];
for (int i = 1; i <= n; i++)
pre_min[i] = min(pre_min[i - 1], a[i] + suf_b[i + 1]);
scanf("%d\n", &q);
while (q--)
{
scanf("%d ", &m);
ll ans = inf, tmp = 0;
for (int i = 1; i <= m; i++)
{
scanf("%d ", &p[i]);
tmp += b[p[i]];
}
p[m + 1] = n + 1;
for (int i = 1; i <= m + 1; i++)
{
ans = min(ans, pre_min[p[i] - 1] - tmp);
tmp -= b[p[i]];
tmp -= c[p[i]];
}
printf("%lld\n", ans);
}
return 0;
}