USACO23FEB G【杂题】
A. [USACO23FEB] Equal Sum Subarrays G
给定一个长为 \(n\) 的数组 \(a\),满足所有子区间的和互不相同。对于所有 \(1 \leq i \le n\),求出最小代价使得对 \(a_i\) 进行一次操作后存在两个子区间的和相同。在一次操作中,你可以用 \(|t-a_i|\) 的代价将 \(a_i\) 修改为任意整数 \(t\)。
\(n \leq 500\),\(|a_i| \le 10^{15}\)。
以下我们称一个区间的权值为区间和。设包含位置 \(i\) 的区间集合为 \(S_i\),不包含 \(i\) 的位置集合为 \(T_i\),一次操作相当于对 \(S_i\) 内的所有区间权值 \(+k\) 或 \(-k\),那么显然这一对权值相同的子区间一定有一个在 \(S_i\) 内,另一个在 \(T_i\) 内。
于是我们的问题变成了已知 \(S_i,T_i\),要求 \(x \in S_i,y \in T_i\) 使得 \(|x-y|\) 最小。排序后双指针扫一下就行了。
但如果对每个 \(i\) 都做一遍,时间复杂度是 \(\mathcal{O}(n^3 \log n)\) 的。我们发现,因为我们能够 \(\mathcal{O}(1)\) 地 check 某个区间是在 \(S_i\) 内还是在 \(T_i\) 内,因此我们只需要保证每次 \(S_i\) 和 \(T_i\) 内的区间都有序就行了,于是只需要在最开始的时候进行一次排序,总时间复杂度 \(\mathcal{O}(n^3)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5e2 + 5;
const LL inf = 1e18;
int n, m; LL a[N], s[N][N];
struct dat {
LL s; int l, r;
bool operator < (const dat &p) const {
return s < p.s;
}
} b[N *N];
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
s[i][i] = a[i];
for (int j = i + 1; j <= n; j++) s[i][j] = s[i][j - 1] + a[j];
}
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
b[++m] = (dat){s[i][j], i, j};
sort(b + 1, b + m + 1);
for (int i = 1; i <= n; i++) {
LL d = inf;
LL lst = -inf;
for (int j = 1; j <= m; j++) {
if (b[j].l <= i && b[j].r >= i) { if (lst != -inf) d = min(d, b[j].s - lst); }
else lst = b[j].s;
}
lst = -inf;
for (int j = m; j >= 1; j--) {
if (b[j].l <= i && b[j].r >= i) { if(lst != -inf) d = min(d, lst - b[j].s); }
else lst = b[j].s;
}
cout << d << "\n";
}
return 0;
}
B. [USACO23FEB] Fertilizing Pastures G
给定一棵 \(n\) 个节点的树,第 \(i(i > 1)\)号节点有生长系数 \(a_i\) 和权值 \(w_i\),初始时 \(w_i = 0\)。
初始时你在 \(1\) 号节点,你需要按照某种顺序遍历所有节点,并使得步数最少。当你第一次到达节点 \(i\) 时,你需要付出 \(w_i\) 的代价,而每当你走一条边时,所有节点都会执行 \(w_i \gets w_i + a_i\)。
对以下两种附加条件 \(T \in \{0,1\}\), 求出满足要求的最小代价。
- \(T=0\),最后要回到 \(1\) 号节点。
- \(T = 1\),最后可以停留在任意节点。
\(n \leq 2 \times 10^5\),\(1 \leq a_i \leq 10^8\)。
先考虑 \(T=0\) 时怎么做。显然,最优的遍历顺序是树的某个 DFS 序,对于 \(n\) 个点的树遍历的步数是 \(2n-2\)。我们只需要对于每个节点 \(u\),决定其儿子的遍历顺序。
设 \(f_u\) 为 \(u\) 子树内的答案,\(sz_u\) 为 \(u\) 子树的大小,\(sum_u\) 为 \(u\) 子树内 \(a_i\) 的和。假设我们现在在 \(u\),先进入了别的子树,经过 \(T\) 时间后再进入 \(v\),我们把每个节点的贡献拆开,可以得到 \(v\) 对 \(f_u\) 的贡献是 \(sum_v \times T + f_v\)。而当我们确定顺序后,遍历某个子树 \(v\) 之前的子树所需的时间是确定的,相当于有 \(k\) 个二元组 \((a_i,b_i)\),其中 \(a_i = sum_{v_i},b_i = 2 \times sz_{v_i}\),求一个排列 \(p\) 使得 \(\sum \limits_{i = 1}^k \left( a_{p_i} \times \big( \sum \limits_{j=1}^{j<i} b_{p_j} + 1 \big) + f_{p_i} \right)\) 最小。
式子中和 \(f\) 有关的项都是确定的,我们要最小化的其实是 \(\sum \limits_{i = 1}^k a_{p_i} \times \sum \limits_{j=1}^{j<i} b_{p_j}\)。这是个经典 exchange argument,考虑序列中相邻的两个位置 \(i,i+1\),显然交换他们不会影响其他项的贡献。设 \(i\) 之前的 \(b_i\) 和为 \(S\),如果交换后答案变得更优,那么有 \(a_i \times S + a_{i+1} \times (S + b_i) > a_{i+1} \times S + a_{i} \times (S + b_{i+1})\),即 \(\frac{a_{i+1}}{b_{i+1}} > \frac{a_i}{b_i}\)。这个条件也是充要的,也就是说,如果序列中存在相邻两个位置满足这个条件,那么交换后答案一定更优。
那么最优的顺序一定按照 \(\frac{a_i}{b_i}\) 降序排列,据此容易计算答案,总时间复杂度 \(\mathcal{O}(n \log n)\)。
对于 \(T = 1\) 有相似的结论,如果我们停留在深度为 \(d\) 的节点,那么遍历的最小步数为 \((2n - 2) - d\)。所以我们最后一定会停留在深度最大的节点。
设最大深度为 \(D\)。同样,我们还是需要为每个节点决定其儿子的遍历顺序,但与之前不同的是,我们需要选择一个儿子 \(v\) 满足其子树内存在深度为 \(D\) 的节点,把 \(v\) 留到最后进入。我们可以先预处理出哪些节点可以作为这样的 \(v\),而对于剩余的儿子节点,和 \(T=1\) 时一样,按照 \(\frac{a_i}{b_i}\) 排序即可。
当然,我们不可能对于每个可能的 \(v\) 都排一遍序。一个简单的优化方式是,由于每次确定 \(v\) 后其余的节点都是按照 \(\frac{a_i}{b_i}\) 排序,因此这和 \(T=0\) 时的结果只会有一项不同,我们可以先算出 \(T=0\) 时的答案,枚举 \(v\) 时扣掉 \(v\) 的贡献,然后加上把 \(v\) 放在最后的贡献。这可以通过一些简单的预处理做到单次 \(\mathcal{O}(1)\)。总时间复杂度 \(\mathcal{O}(n \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5e5 + 5;
const LL inf = 2e18;
int n, t, mxd, a[N], sz[N], dep[N]; LL sum[N], f[N], g[N], suf[N];
bool mark[N];
vector <int> e[N];
struct dat {
LL a, b; int v;
} d[N];
void dfs0(int u) {
for (auto v : e[u]) dfs0(v), dep[u] = max(dep[u], dep[v] + 1);
}
void ptag(int u, int d) {
if (d + dep[u] == mxd) mark[u] = 1;
for (auto v : e[u]) ptag(v, d + 1);
}
void dfs(int u) {
sz[u] = 1, sum[u] = a[u];
int m = 0;
for (auto v : e[u]) {
dfs(v);
sz[u] += sz[v], sum[u] += sum[v];
f[u] += f[v];
}
for (auto v : e[u]) {
d[++m] = (dat){ sum[v], 2 * sz[v], v };
}
sort(d + 1, d + m + 1, [&](dat i, dat j) { return j.a * i.b < i.a * j.b; });
LL val = 0, sb = 1;
for (int i = 1; i <= m; i++) {
val += d[i].a * sb, sb += d[i].b;
}
suf[m + 1] = 0;
for (int i = m; i >= 1; i--) suf[i] = suf[i + 1] + d[i].a;
LL ret = inf, pre = 1;
for (int i = 1; i <= m; i++) {
int v = d[i].v;
if (mark[v]) ret = min(ret, f[u] - f[v] + g[v] + val - d[i].b * suf[i + 1] - d[i].a * pre + d[i].a * (sb - d[i].b));
pre += d[i].b;
}
f[u] += val, g[u] = ret;
if (m == 0) g[u] = 0;
}
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> t;
for (int i = 2, ff; i <= n; i++) {
cin >> ff >> a[i];
e[ff].push_back(i);
}
dfs0(1), mxd = 0;
for (int i = 1; i <= n; i++) mxd = max(mxd, dep[i]);
ptag(1, 0), dfs(1);
if (t == 0) cout << 2 * (n - 1) << " " << f[1] << "\n";
else cout << 2 * (n - 1) - mxd << " " << g[1] << "\n";
return 0;
}
C. [USACO23FEB] Piling Papers G
给定长度为 \(n\) 的序列 \(a\),和区间 \([A,B]\), \(q\) 次询问,每次询问给出 \(l,r\)。每次询问开始,你有一个空串 \(x\),你可以从 \(l\) 到 \(r\) 依次进行操作,操作有三种:\(x\rightarrow\overline{x+a_i}\),\(x\rightarrow\overline{a_i+x}\),或者什么也不做,求 \(\overline{x}\) 的取值在 \([A,B]\) 的不同的操作方案数。答案对 \(10^9+7\) 取模。
\(n \leq 300\),\(q \leq 5 \times 10^4\),\(1 \leq a_i \leq 9\),\(1 \leq A \leq B < 10^{18}\)。
先考虑一次询问怎么做。首先容斥成 \(\leq B\) 的答案减去 \(\leq A-1\) 的答案,因此我们只考虑 \(\leq t\) 的答案怎么求。
一个朴素的想法是设 \(f_{i,j,0/1/2}\) 表示前 \(i\) 个数,从低位开始填了 \(j\) 位,此时和 \(t\) 的最低 \(j\) 位的大小关系是小于,等于或大于的方案数。但你发现,虽然在做 \(x\rightarrow\overline{x+a_i}\) 的时候我们可以 \(\mathcal{O}(1)\) 地得到大小关系,但是在 \(x\rightarrow\overline{a_i+x}\) 时并没有办法靠谱地确定,除非我们直接记录下当前填出的 \(x\),但这样状态就爆炸了。
这给我们一些启示:在设计状态的时候,我们不能让 \(x\) 在 \(t\) 上对应的区间移动。自然想到设 \(f_{i,l,r,0/1/2}\) 表示前 \(i\) 个数,形成的 \(r - l +1\) 位的数字 \(x\),和 \(t\) 从高到低的第 \(l\) 位到第 \(r\) 位形成的数字 \(t'\) 的大小关系是小于,等于或大于的方案数,转移只需要考虑往哪个方向扩展。此时我们就可以只通过当前位确定出整个区间的大小关系了,于是可以 \(\mathcal{O}(1)\) 完成转移。
设 \(t\) 的位数为 \(d\),那么最后的答案是 \(f_{n,1,d,0} + f_{n,1,d,1} + \sum \limits_{j=2}^d \sum \limits_{k=0}^2 f_{n,j,d,k}\),其中最后一项计算的是位数 \(<d\) 的方案数。
于是我们得到了一个 \(\mathcal{O}(qn \log^2 B)\) 的做法。但我们发现,每次 DP 的时候我们其实都计算出了每个前缀的答案,因此我们对于每个后缀做一次 DP,查询的时候用前缀和 \(\mathcal{O}(1)\) 查询,总时间复杂度就变成了 \(\mathcal{O}(n^2 \log^2 B + q)\),足以通过本题。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e2 + 5, M = 20, mod = 1e9 + 7;
int n, m, q, a[N], lim[M], ansL[N][N], ansR[N][N], f[M][M][3]; LL L, R;
void getlim(LL x) {
m = 0;
while (x > 0) lim[++m] = x % 10, x = x / 10;
reverse(lim + 1, lim + m + 1);
}
int chk(int a, int b) {
if (a < b) return 0;
else if (a == b) return 1;
else return 2;
}
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> L >> R;
for (int i = 1; i <= n; i++) cin >> a[i];
getlim(L - 1);
for (int i = 1; i <= n; i++) {
memset(f, 0, sizeof(f));
for (int j = i; j <= n; j++) {
for (int x = 1; x <= m; x++) {
for (int y = m; y > x; y--) {
if (a[j] > lim[x]) {
for (int k = 0; k <= 2; k++) f[x][y][2] = (f[x][y][2] + f[x + 1][y][k]) % mod;
} else if (a[j] == lim[x]) {
for (int k = 0; k <= 2; k++) f[x][y][k] = (f[x][y][k] + f[x + 1][y][k]) % mod;
} else {
for (int k = 0; k <= 2; k++) f[x][y][0] = (f[x][y][0] + f[x + 1][y][k]) % mod;
}
f[x][y][2] = (f[x][y][2] + f[x][y - 1][2]) % mod;
f[x][y][chk(a[j], lim[y])] = (f[x][y][chk(a[j], lim[y])] + f[x][y - 1][1]) % mod;
f[x][y][0] = (f[x][y][0] + f[x][y - 1][0]) % mod;
}
}
for (int x = 1; x <= m; x++) f[x][x][chk(a[j], lim[x])] = (f[x][x][chk(a[j], lim[x])] + 2) % mod;
for (int x = 1; x <= m; x++) {
ansL[i][j] = (ansL[i][j] + f[x][m][1]) % mod;
ansL[i][j] = (ansL[i][j] + f[x][m][0]) % mod;
if (x > 1) ansL[i][j] = (ansL[i][j] + f[x][m][2]) % mod;
}
}
}
getlim(R);
for (int i = 1; i <= n; i++) {
memset(f, 0, sizeof(f));
for (int j = i; j <= n; j++) {
for (int x = 1; x <= m; x++) {
for (int y = m; y > x; y--) {
if (a[j] > lim[x]) {
for (int k = 0; k <= 2; k++) f[x][y][2] = (f[x][y][2] + f[x + 1][y][k]) % mod;
} else if (a[j] == lim[x]) {
for (int k = 0; k <= 2; k++) f[x][y][k] = (f[x][y][k] + f[x + 1][y][k]) % mod;
} else {
for (int k = 0; k <= 2; k++) f[x][y][0] = (f[x][y][0] + f[x + 1][y][k]) % mod;
}
f[x][y][2] = (f[x][y][2] + f[x][y - 1][2]) % mod;
f[x][y][chk(a[j], lim[y])] = (f[x][y][chk(a[j], lim[y])] + f[x][y - 1][1]) % mod;
f[x][y][0] = (f[x][y][0] + f[x][y - 1][0]) % mod;
}
}
for (int x = 1; x <= m; x++) f[x][x][chk(a[j], lim[x])] = (f[x][x][chk(a[j], lim[x])] + 2) % mod;
for (int x = 1; x <= m; x++) {
ansR[i][j] = (ansR[i][j] + f[x][m][1]) % mod;
ansR[i][j] = (ansR[i][j] + f[x][m][0]) % mod;
if (x > 1) ansR[i][j] = (ansR[i][j] + f[x][m][2]) % mod;
}
}
}
cin >> q;
while (q--) {
int l, r; cin >> l >> r;
int ans = (ansR[l][r] - ansL[l][r] + mod) % mod;
cout << ans << "\n";
}
return 0;
}