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;
}
posted @ 2023-03-18 20:32  came11ia  阅读(40)  评论(0编辑  收藏  举报