USACO2024 Jan 合集
Platinum
啥也不会。官方题解写的很好。
T1
题解没看懂。不会仙人掌不会生成函数。
T2
题意: 有一行 \(n\) 个石子,大小为 \(s_1,\cdots,s_n\),每次等概率挑选一对相邻的石子 \(a\) 和 \(b\) 合并,新编号等于大小更大的石子的编号(如果大小相等就是更大的编号为新编号),最后剩下的编号是 \(i\) 的概率为 \(f_i\),求 \(1 \sim f_n\)。\(n \le 5000\)
有意思的区间 DP。
设 \(sum(l, r) = \sum_{l \le i \le r}s_i\)
枚举最后剩下的 \(c\),显然任何时候 \(c\) 都存在,我们考虑区间 \([l, r]\) 合并之后 \(c\) 是最后剩下的概率是 \(dp[l][r]\)。最后一次合并,等概率选取一个分界点,对于包含 \(c\) 的,我们会计算 \(dp\) 更小的区间。所有得到:
初始是 \(dp[c][c]=1\),答案是 \(dp[1][n]\)。大于等于号取决于编号。
计算的时间复杂度是 \(O(n^3)\) 的,加上枚举 \(c\) 就是 \(O(n^4)\),当然可以用前缀和优化,最多 \(O(n^3)\),不好做。
如果要区间 DP,复杂度至少 \(O(n^2)\),但是我们还要枚举 \(c\),所以我们考虑如何不枚举 \(c\)。
我们反过来,定义 \(dp[l][r]\) 表示最终答案属于 \([l, r]\) 的概率,这样每个 \(dp[c][c]\) 就是答案,初始状态是 \(dp[1][n] = 1\)。类似上面我们可以得到转移方程:
两个部分看着很对称,我们先考虑优化第一部分。
不妨设 \(pL[l][r]\) 表示 \(dp[l][r]\) 是 \(p\) 最小是多少。
我们现在相当于固定 \(r\),求一个 \(pL[l][r] \le p < l\) 的 \(\frac{dp[p][r]}{r-p}\) 之和,考虑到长度从大到小,所以 \(l\) 是递增的,每次新加入的一定在最后,所以我们可以预处理一个前缀和计算。
再考虑到 \(sum(p, l - 1) \le sum(l, r)\) 的限制,可以得到 \(pL[l][r] \le pL[l + 1][r]\),决策具有单调性,于是我们可以根据这个在 \(O(n^2)\) 内算出所有的决策点。于是我们可以在 \(O(n^2)\) 解决这个问题。
点击查看代码
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5005;
const int mod = 1e9 + 7;
int n;
int a[N] = {0}, s[N] = {0};
int sum(int l, int r) {
return s[r] - s[l - 1];
}
int inv[N] = {0};//线性求逆元
void init() {
inv[1] = 1;
for (int i = 2; i <= n; i++)
inv[i] = (1ll * (mod - (mod / i)) * inv[mod % i]) % mod;
}
int pL[N][N] = {{0}};
int pR[N][N] = {{0}};
void cal() {
for (int r = 1; r <= n; r++) {
int cur = 1;
for (int l = 1; l <= r; l++) {
while (cur < l && sum(cur, l - 1) > sum(l, r))
cur++;
pL[l][r] = cur;
}
}
for (int l = 1; l <= n; l++) {
int cur = n;
for (int r = n; r >= l; r--) {
while (cur > r && sum(r + 1, cur) >= sum(l, r))
cur--;
pR[l][r] = cur;
}
}
}
int dp[N][N] = {{0}};
vector<int> sl[N], sr[N];
//计算dp[l][p]/p - l
//计算dp[p][r]/r - p
int main() {
cin >> n;
init();
for (int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
//先算决策点
cal();
//转移
dp[1][n] = 1;
for (int i = 1; i <= n; i++) {
sl[i].push_back(0);
sr[i].push_back(0);
}
sl[n].push_back(1ll * dp[1][n] * inv[n - 1] % mod);
sr[1].push_back(1ll * dp[1][n] * inv[n - 1] % mod);
for (int i = n - 1; i >= 1; i--) {
for (int l = 1; l + i - 1 <= n; l++) {
int r = l + i - 1;
dp[l][r] = 0;
int lenL = (int)sl[r].size(), ansL = 0;//前缀和的长度
int lenVldL = max(l - pL[l][r], 0);
ansL = (sl[r][lenL - 1] - sl[r][lenL - 1 - lenVldL] + mod) % mod;
int lenR = (int)sr[l].size(), ansR = 0;
int lenVldR = max(pR[l][r] - r, 0);
ansR = (sr[l][lenR - 1] - sr[l][lenR - 1 - lenVldR] + mod) % mod;
dp[l][r] = (ansL + ansR) % mod;
}
for (int r = i; r <= n; r++) {
int l = r - i + 1;
sl[r].push_back(((1ll * dp[l][r] * inv[r - l] % mod) + sl[r].back()) % mod);
sr[l].push_back(((1ll * dp[l][r] * inv[r - l] % mod) + sr[l].back()) % mod);
}
}
for (int i = 1; i <= n; i++)
cout << dp[i][i] << endl;
return 0;
}
T3
题意: 平面上 \(n\) 个点 \((x_i, y_i)\),其中纵坐标和横坐标都是 \(1 \sim n\) 的排列,求有多少种方案,选出非空集合 \(S,T \sub [n]\) 且 \(S \cap T = \empty\),使得存在一条平行于横轴或纵轴的直线,分开了 \(S\) 和 \(T\) 中的点。\(n \le 10^5\)
考场不够冷静(全想着打麻将了),只推出了最开始的一部分。
我们考虑分别按照 \(x\) 和 \(y\) 枚举所有 \(S\) 都在 \(T\) 左边和 \(S\) 都在 \(T\) 上面,再减去 \(S\) 在 \(T\) 的右上方,最后乘 2 就是答案。
我们先按 \(x\) 排序(\(y\) 同理),则假设 \(S\) 最右边的是 \(i\),贡献就是 \(2^{i-1}(2^{n-i}-1)\),因为 \(T\) 非空。
我们在考虑怎么求 \(S\) 在 \(T\) 的右上的。
这时我们关心两个事儿:\(x\) 分界线和 \(y\) 分界线。我们考虑按 \(x\) 排序,再枚举 \(y\)。
我们记 \(F_i(y)\) 表示 \(S\) 中 \(S\) 最右的是 \(x_i\),并且所有点的 \(y_j\) 最小值等于 \(y\)。
我们设 \(a_i(y)\) 表示所有满足 \(j < i\) 且 \(y_j \le y\) 的 \(j\) 的个数。
\(b_i(y)\) 表示所有满足 \(j > i\) 且 \(y_j < y\) 的个数。
分三种情况:
\(y > y_i\),显然最小值会比 \(y\) 小,所以 \(F_i(y)=0\)。
\(y = y_i\),\(F_i(y)=2^{a_i(y)}(2^{b_i(y)}-1)\)。
\(y < y_i\),\(F_i(y)=(2^{a_i(y)}-2^{a_i(y+1)})(2^{b_i(y)}-1)\),这是为了保证最小值恰好为 \(y\)。
答案就是 \(\sum_i\sum_yF_i(y)\)。
现在我们考虑用数据结构优化求和。
拆贡献,以上三种情况取决于三个函数:\(2^{a_i(y)},2^{a_i(y)+{b_i(y)}},2^{a_i(y + 1)+{b_i(y)}}\)。最后 \(F_i(y)\) 就是它们的线性组合。所以我们考虑维护这三个函数的和。
显然这和 \(a_i(y)\) 和 \(b_i(y)\) 的增减有关,可以转化为对这三个函数的乘除。所以我们考虑 \(a_i(y)\) 和 \(b_i(y)\) 的变化。
当 \(i\) 变成 \(i+1\),我们发现所有 \(y \le y_i\) 的 \(y\) 都使 \(a_i(y)\) 增加 \(1\)。而所有 \(y > y_{i+1}\) 的 \(b_i(y)\) 都减少 \(1\)。
所以我们用线段树维护区间乘法和区间和即可解决,时间复杂度 \(O(n \log n)\)。
还有一个细节,我们不只要算 \(S\) 在 \(T\) 右上的,还要算右下的,这是应为两种可以交换得到,所以我们要将 \(y\) 值取反再算一遍。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e5 + 5;
const int mod = 1e9 + 7;
const int inv2 = (mod + 1) / 2;
struct SegTree {
int sz;
int val[N * 4];
int tag[N * 4];
#define ls (x << 1)
#define rs (x << 1 | 1)
#define mid ((lx + rx) >> 1)
void pushup(int x) {
val[x] = (val[ls] + val[rs]) % mod;
}
void pushdown(int x) {
if (tag[x] == 1)
return;
val[ls] = (1ll * val[ls] * tag[x]) % mod;
val[rs] = (1ll * val[rs] * tag[x]) % mod;
tag[ls] = (1ll * tag[ls] * tag[x]) % mod;
tag[rs] = (1ll * tag[rs] * tag[x]) % mod;
tag[x] = 1;
}
void build(int x, int lx, int rx, vector<int> &a) {
tag[x] = 1;
if (lx + 1 == rx) {
val[x] = a[lx - 1];
return;
}
build(ls, lx, mid, a), build(rs, mid, rx, a);
pushup(x);
}
void upd(int x, int lx, int rx, int l, int r, int v) {
if (rx <= l || r <= lx)
return;
if (l <= lx && rx <= r) {
val[x] = (1ll * val[x] * v) % mod;
tag[x] = (1ll * tag[x] * v) % mod;
return;
}
pushdown(x);
upd(ls, lx, mid, l, r, v), upd(rs, mid, rx, l, r, v);
pushup(x);
}
int qry(int x, int lx, int rx, int l, int r) {
if (rx <= l || r <= lx)
return 0;
if (l <= lx && rx <= r)
return val[x];
pushdown(x);
return (qry(ls, lx, mid, l, r) + qry(rs, mid, rx, l, r)) % mod;
}
SegTree () {}
SegTree (vector<int> &a) {
sz = (int)a.size();
build(1, 1, sz + 1, a);
}
#undef ls
#undef rs
#undef mid
} st1, st2, st3;
//st1 储存 2^a(y)
//st2 储存 2^(a(y)+b(y))
//st3 储存 2^(a(y+1)+b(y))
int n;
struct Pnt {
int x, y;
Pnt (int _x = 0, int _y = 0) :
x(_x), y(_y) {}
} p[N];
bool cmp(Pnt a, Pnt b) {
return a.x < b.x;
}
int pw[N] = {0};
vector<int> a;
int slv() {
//对于 2^a(y),最开始全是1
a = vector<int>(n, 0);
for (int i = 0; i < n; i++)
a[i] = 1;
st1 = SegTree(a);
//对于 2^a(y)+b(y),最开始就是 2^b(y)
//而 b(y) 为所有小于 y 的个数
for (int i = 0; i < n; i++)
a[i] = 0;
for (int i = 2; i <= n; i++)
a[p[i].y - 1]++;
for (int i = 1; i < n; i++)
a[i] += a[i - 1];
for (int i = n - 1; i >= 1; i--)
a[i] = pw[a[i - 1]];
a[0] = 1;
st2 = SegTree(a);
//对于 2^a(y+1)+b(y),依然是 2^b(y)
st3 = SegTree(a);
int res = 0;
for (int i = 1; i <= n; i++) {
//对于 y = p[i].y 的贡献是 2^(a(y)+b(y)) - 2^a(y)
res = (res + (st2.qry(1, 1, n + 1, p[i].y, p[i].y + 1) - st1.qry(1, 1, n + 1, p[i].y, p[i].y + 1) + mod) % mod) % mod;
//对于 y < p[i].y 的贡献为 2^(a(y)+b(y)) - 2^a(y) - 2^(a(y+1)+b(y))+2^(a(y + 1))
//考虑 a(y + 1) 就是不包括 a(2) 到 a(p[i].y) 的所有之和
res = (res + st2.qry(1, 1, n + 1, 1, p[i].y)) % mod;
res = (res + st1.qry(1, 1, n + 1, 2, p[i].y + 1)) % mod;
res = (res - st1.qry(1, 1, n + 1, 1, p[i].y) + mod) % mod;
res = (res - st3.qry(1, 1, n + 1, 1, p[i].y) + mod) % mod;
if (i == n)
break;
//对于 2^a(y) 和 2^a(y)+b(y) 来说,所有 y <= p[i].y 的 y 都要乘2
st1.upd(1, 1, n + 1, 1, p[i].y + 1, 2);
st2.upd(1, 1, n + 1, 1, p[i].y + 1, 2);
//对于 2^a(y)+b(y) 和 2^a(y+1)+b(y),所有 y > p[i + 1].y 的 y 都要除2
st2.upd(1, 1, n + 1, p[i + 1].y + 1, n + 1, inv2);
st3.upd(1, 1, n + 1, p[i + 1].y + 1, n + 1, inv2);
//对与 2^a(y+1)+b(y) 来说,所有 [1, p[i].y - 1] 要乘 2
st3.upd(1, 1, n + 1, 1, p[i].y, 2);
}
return res;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> p[i].x >> p[i].y;
//先算分别的
int ans = 0;
pw[0] = 1;
for (int i = 1; i <= n; i++)
pw[i] = (1ll * pw[i - 1] * 2) % mod;
for (int i = 1; i <= n; i++)
ans = (ans + (1ll * pw[i - 1] * (pw[n - i] - 1 + mod)) % mod) % mod;
ans = (ans * 2ll) % mod;
//再算重叠的
sort(p + 1, p + n + 1, cmp);
int res = slv();
for (int i = 1; i <= n; i++)
p[i].y = n - p[i].y + 1;
res = (res + slv()) % mod;
cout << (ans - res + mod) % mod * 2ll % mod << endl;
return 0;
}