牛客OI周赛9-提高组题目记录
昨天晚上做了这一套比赛,觉得题目质量挺高,而且有一些非常有趣而且非常清奇的脑回路在里边,于是记录在此。
T1: 扫雷
设 \(f_i\) 表示扫到第 \(i\) 个雷的期望用时,那么我们要求的答案就是 \(f_n\)。
我们不难写出一个递推式:
\[ f_{i +1} = \left((f_i +1) \cdot \dfrac{a_i}{b_i}\right) + \left(2(f_i + 1) \cdot \dfrac{b_i - a_i}{b_i} \cdot \dfrac{a_i}{b_i} \right) + \left(3(f_i + 1) \cdot \dfrac{b_i - a_i}{b_i} \cdot \dfrac{b_i - a_i}{b_i} \cdot \dfrac{a_i}{b_i} \right) + \cdots \]
提出公因式 \(\left((f_i +1) \cdot \dfrac{a_i}{b_i}\right)\),我们可以将式子整理成:
\[ f_{i +1} = \left((f_i +1) \cdot \dfrac{a_i}{b_i}\right) \times \left(\sum_{k = 0}^{\infty} (k + 1)\cdot\left(\dfrac{b_i - a_i}{b_i}\right)^k\right) \]
后面的式子其实就等于 \(\left(\dfrac{b_i}{a_i}\right)^2\),以下是我的一些推导:
设 \(x = \dfrac{b_i - a_i}{b_i}\),则我们要求的式子 \(S\) 可以表示为:
\[ S = 1 + 2x + 3x^2 +4x^3 +\cdots \]
左右两边乘上 \(x\),则有
\[ xS = x + 2x^2 + 3x^3 + 4x^4 + \cdots \]
上式减去下式,则可以得到
\[ S = \dfrac{1 + x + x^2 + x^3 + x^4 +\cdots}{1 - x} \]
我们设 \(S' = 1 + x + x^2 + x^3 +\cdots\),也就是上式中的分子,那么左右两边乘上 \(x\),有
\[ xS' = x + x^2 + x^3 + x^4 + x^5 + \cdots \]
同样的方法相减,可以得到
\[ S' = \dfrac{1}{1 - x} \]
然后代入 \(S\),则有
\[ S = \dfrac{1}{(1 - x)^2} \]
再将 \(x = \dfrac{b_i - a_i}{b_i}\) 代入 \(S\),稍加运算即可解出
\[ S = \left(\dfrac{b_i}{a_i}\right)^2 \]
于是我们求出了这个非常简单的递推式:
\[ f_{i + 1} = f_i \cdot \dfrac{b_i}{a_i} \]
边递推边算逆元即可,时间复杂度 \(\mathcal{O}(n \log a_i)\)。
代码:
#include <cstdio>
const int MaxN = 1000000 + 5;
const int Mod = 1000000007;
int N;
int A[MaxN], B[MaxN];
inline int add(int x, int y) { return (x += y) >= Mod ? x - Mod : x; }
inline int mul(int x, int y) { return 1LL * x * y % Mod; }
inline int pw(int x, int y) { int z = 1; for (; y; y >>= 1, x = mul(x, x)) if (y & 1) z = mul(z, x); return z; }
inline int inv(int x) { return pw(x, Mod - 2); }
inline int sep(int x, int y) { return mul(x, inv(y)); }
void init() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%d %d", &A[i], &B[i]);
}
void solve() {
int f = 0;
for (int i = 1; i <= N; ++i)
f = mul(add(f, 1), sep(B[i], A[i]));
printf("%d\n", f);
}
int main() {
init();
solve();
return 0;
}
PS
这题的 \(n\) 好像可以开到 \(10^7\)。(忽略读入的时间)
这里的 \(a_i\) 全部都知道了,可以在 \(\mathcal{O}(n + \log p)\) 的时间预处理出所有 \(a_i\) 的逆元。
大致做法就是求出 \(a_i\) 的前缀积数组 \(mul_i\),然后将 \(mul_n\) 拿去求逆元,得到 \(mul_n^{-1}\),然后 \(mul_n^{-1} \cdot mul_{n - 1}\) 就是 \(a_{n}^{-1}\);接下来将 \(mul_n^{-1}\) 乘上 \(a_n\) 就可以得到 \(mul_{n - 1}^{-1}\),继续做类似的事情即可,推回到 \(1\),就得到了所有 \(a_i\) 的逆元。
T2: 情书
我的做法好像和大家不太一样?
答案就是区间中每个子串答案之和除以子串个数,于是我们只需要求出每个字串的心动值之和即可。
首先有一个优雅的暴力。我们不枚举子串,只考虑这个区间内每个字符对答案的贡献。
作为子串中从左往右的第 \(i\) 个字符,假设值为 \(c\),那么这个字符对右边的每个位置,都能延伸出这么长的一个子串;对于左边的每个位置,都不妨碍右边的取值,即为一个倍数关系。
稍加思考可以写出答案的式子:(\(b\) 为题中给出的心动程度)
\[ ans = 1 \times c_1 \times \left(\sum_{i = 0}^{n - 1} b^i\right) + 2 \times c_2 \times \left(\sum_{i = 0}^{n - 2} b^i\right) + 3 \times c_3 \times \left(\sum_{i = 0}^{n - 3} b^i\right) + \cdots + n \times c_n \times \left(\sum_{i = 0}^{n - n} b^i\right) \]
这个东西显然是可以递推求的,而且修改也非常方便,直接修改对应的 \(c\) 值就好了。
但是多次询问,如果快速求出一个区间的答案呢?
注意到这个东西是可以递推的,也就是说我如果求出了前 \(i - 1\) 个的答案,我可以在 \(\mathcal{O}(1)\) 的时间内求出前 \(i\) 个的答案。
这说明在这个问题上,增量法是可以使用的。
我们又发现,对于一段区间,我们会重复扫过这些点很多次。于是我们希望处理出一些区间的答案。
这不难让人想到线段树。
但一个非常棘手的问题就出现了:如果我们求出了两个区间的答案,可以合并吗?又该如何合并?
(下面为了叙述直观,将采用一些不严谨的表达方式,请见谅)
为了方便找到规律,我们抛弃这些冗杂且抽象的式子,转为具体的——左右区间都只有三个元素。
我们设左区间的答案为 \(ans_l\),右区间的答案为 \(ans_r\)。
不难写出 \(ans_l\) 和 \(ans_r\) 的表达式:
\[ ans_l = c_1 \cdot 111 + 2 \cdot c_2 \cdot 11 + 3 \cdot c_2 \cdot 1 \\ ans_r = c_4 \cdot 111 + 2 \cdot c_5 \cdot 11 + 3 \cdot c_6 \cdot 1 \]
(其中的 \(111, 11, 1\) 都是 \(b\) 进制下的数,为了叙述直观,故如此表示。)
我们要求的 \(ans\) 值,应当是
\[ ans = c_1 \cdot 111111 + 2 \cdot c_2 \cdot 11111 + 3 \cdot c_3 \cdot 1111 + 4 \cdot c_4 \cdot 111 +5 \cdot c_5 \cdot 11 + 6 \cdot c_6 \cdot 1 \]
分别考虑左区间和右区间对这个最终值的贡献。显然最终值中含 \(c_1, c_2, c_3\) 的这三个单项式是从左区间贡献来的,而含 \(c_4, c_5, c_6\) 的单项式是从右区间贡献来的。
那么我们观察一下,从 \(c_1 \cdot 111 + 2 \cdot c_2 \cdot 11 + 3 \cdot c_2 \cdot 1\) 到 \(c_1 \cdot 111111 + 2 \cdot c_2 \cdot 11111 + 3 \cdot c_3 \cdot 1111\),这三项经历了什么。
首先它们全部乘上了 \(b^{rsize}\),其中 \(rsize\) 是右区间的长度,在这个例子中等于 \(3\)。
然后它们全部加上了 \(111 \times(c_1 + 2 \cdot c_2 + 3 \cdot c_3)\)。
于是我们发现我们又需要多递推两个式子:区间长度以及“区间带权和”(即 \(\sum_{i = 1}^n i \cdot c_i\))。
区间带权和的递推请自行脑补,或者参考后面代码中 Merge()
函数中的 \(c\) 项,指的就是区间带权和。值得一提的是,在递推这个的过程中,您会发现,我们还需要递推一个区间和,即 \(\sum_{i = 1}^n c_i\),对应 Merge()
函数中的 \(d\) 项。
再考虑右区间的贡献,从 \(c_4 \cdot 111 + 2 \cdot c_5 \cdot 11 + 3 \cdot c_6 \cdot 1\) 到 \(4 \cdot c_4 \cdot 111 +5 \cdot c_5 \cdot 11 + 6 \cdot c_6 \cdot 1\),这三项又经历了什么。
它们加上了 \(lsize \times (c_4 \cdot 111 + c_5 \cdot 11 +c_6 \cdot 1)\)。
于是我们发现我们又需要多递推一个式子:“区间不带权答案”(即 \(c_1 \cdot 11111... + c_2 \cdot 1111... + c_3 \cdot 111... + \cdots\))
区间不带权答案的递推类似,您可以参考后面代码中 Merge()
函数的 \(b\) 项的递推过程。
然后一堆数一起递推上去,合并一下即可。
这样 \(\texttt{pushup()}\) 函数终于完成了,其它方面就类似线段树做就好了。更新就单点更新然后 \(\texttt{pushup}\) 到根,询问就区间询问,然后如果有分隔区间的话调用 Merge()
函数合并一下答案即可。
时间复杂度 \(\mathcal{O}(n \log n)\),空间是卡着过的。
代码:第一次写有 14 个参数的函数,哭了
#include <cstdio>
const int MaxN = 200000 + 5, MaxV = 800000 + 5;
const int Mod = 1000000007;
inline int add(int x, int y) { return (x += y) >= Mod ? x - Mod : x; }
inline int mul(int x, int y) { return 1LL * x * y % Mod; }
inline int pw(int x, int y) { int z = 1; for (; y; y >>= 1, x = mul(x, x)) if (y & 1) z = mul(z, x); return z; }
inline int inv(int x) { return pw(x, Mod - 2); }
inline int sep(int x, int y) { return mul(x, inv(y)); }
int N, M, B;
int A[MaxN], F[MaxN], G[MaxN];
struct SegTree {
int L[MaxV], R[MaxV];
int Ans[MaxV], Sum[MaxV], Wht[MaxV], Wth[MaxV];
inline void Merge(int &a0, int &b0, int &c0, int &d0, int a1, int b1, int c1, int d1, int a2, int b2, int c2, int d2, int lsize, int rsize) {
d0 = add(d1, d2);
c0 = add(c1, add(c2, mul(lsize, d2)));
b0 = add(add(mul(b1, F[rsize]), mul(d1, G[rsize])), b2);
a0 = add(add(mul(a1, F[rsize]), mul(c1, G[rsize])), add(a2, mul(lsize, b2)));
}
inline void pushup(int i) {
int ls = i << 1, rs = i << 1 | 1;
int lSize = R[ls] - L[ls] + 1, rSize = R[rs] - L[rs] + 1;
Merge(Ans[i], Wth[i], Wht[i], Sum[i], Ans[ls], Wth[ls], Wht[ls], Sum[ls], Ans[rs], Wth[rs], Wht[rs], Sum[rs], lSize, rSize);
}
void Build_Tree(int left, int right, int i) {
L[i] = left, R[i] = right;
if (left == right) {
Sum[i] = Ans[i] = Wht[i] = Wth[i] = A[left];
return;
}
int mid = (L[i] + R[i]) >> 1;
Build_Tree(left, mid, i << 1);
Build_Tree(mid + 1, right, i << 1 | 1);
pushup(i);
}
void Update_Tree(int pos, int val, int i) {
if (L[i] == R[i]) {
Sum[i] = Ans[i] = Wht[i] = Wth[i] = val;
return;
}
int mid = (L[i] + R[i]) >> 1;
if (pos <= mid) Update_Tree(pos, val, i << 1);
else Update_Tree(pos, val, i << 1 | 1);
pushup(i);
}
void Query_Tree(int left, int right, int i, int &a, int &b, int &c, int &d) {
if (left == L[i] && right == R[i]) {
a = Ans[i], b = Wth[i], c = Wht[i], d = Sum[i];
return;
}
int mid = (L[i] + R[i]) >> 1;
if (right <= mid) Query_Tree(left, right, i << 1, a, b, c, d);
else if (left > mid) Query_Tree(left, right, i << 1 | 1, a, b, c, d);
else {
int a1, b1, c1, d1, a2, b2, c2, d2;
Query_Tree(left, mid, i << 1, a1, b1, c1, d1);
Query_Tree(mid + 1, right, i << 1 | 1, a2, b2, c2, d2);
Merge(a, b, c, d, a1, b1, c1, d1, a2, b2, c2, d2, mid - left + 1, right - mid);
}
}
} T;
void init() {
scanf("%d %d %d", &N, &M, &B);
char str[MaxN];
scanf("%s", str + 1);
for (int i = 1; i <= N; ++i) A[i] = str[i] - 'a' + 1;
F[0] = 1; G[0] = 0;
for (int i = 1; i <= N; ++i) {
F[i] = mul(F[i - 1], B);
G[i] = add(G[i - 1], F[i - 1]);
}
}
void solve() {
T.Build_Tree(1, N, 1);
for (int i = 1; i <= M; ++i) {
int opt; scanf("%d", &opt);
if (opt == 0) {
int a, b, c, d, l, r;
scanf("%d %d", &l, &r);
T.Query_Tree(l, r, 1, a, b, c, d);
int substrings = mul(r - l + 1, sep(r - l + 2, 2));
printf("%d\n", sep(a, substrings));
} else {
int x; char c;
scanf("%d %c", &x, &c);
T.Update_Tree(x, c - 'a' + 1, 1);
}
}
}
int main() {
init();
solve();
return 0;
}
T3: 学习
当时没有想出正解嘤嘤嘤。
对于 \(30\%\) 的数据 \(n ^ 2\) 暴力一下,也就是枚举每一个点,然后枚举子树内的数 \(\mathcal{O}(n)\) 转移一下。
对于一条链的情况,是个三维偏序问题,保证了 \(i < j\) 有序,然后 CDQ 套树状数组做一下就好了。
暴力代码:
#include <algorithm>
#include <cstdio>
const int MaxN = 100000 + 5;
struct Graph {
int cnte;
int Head[MaxN], To[MaxN], Next[MaxN];
inline void add_edge(int from, int to) {
cnte++; To[cnte] = to;
Next[cnte] = Head[from]; Head[from] = cnte;
}
};
int N;
int A[MaxN], B[MaxN], C[MaxN];
int Da[MaxN], Db[MaxN];
Graph G; bool isChain = true;
void init() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) {
scanf("%d %d %d", &A[i], &B[i], &C[i]);
Da[i] = A[i], Db[i] = B[i];
}
std::sort(Da + 1, Da + 1 + N);
std::sort(Db + 1, Db + 1 + N);
for (int i = 1; i <= N; ++i) {
A[i] = std::lower_bound(Da + 1, Da + 1 + N, A[i]) - Da;
B[i] = std::lower_bound(Db + 1, Db + 1 + N, B[i]) - Db;
}
for (int i = 1; i < N; ++i) {
int u, v;
scanf("%d %d", &u, &v);
if (u != v - 1) isChain = false;
G.add_edge(u, v);
}
}
// n <= 1000
namespace Subtask1 {
int cntv;
int id[MaxN], dfn[MaxN], siz[MaxN];
int dp[MaxN];
void dfs(int u) {
siz[u] = 1;
id[u] = ++cntv, dfn[cntv] = u;
for (int i = G.Head[u]; i; i = G.Next[i]) {
int v = G.To[i];
dfs(v);
siz[u] += siz[v];
}
dp[u] = C[u];
for (int i = id[u] + 1; i <= id[u] + siz[u] - 1; ++i) {
int v = dfn[i];
if (A[v] <= A[u] && B[v] <= B[u])
dp[u] = std::max(dp[u], dp[v] + C[u]);
}
}
void solve() {
dfs(1);
int ans = 0;
for (int i = 1; i <= N; ++i) ans = std::max(ans, dp[i]);
printf("%d\n", ans);
}
}
// chain
namespace Subtask2 {
int dp[MaxN], lnk[MaxN];
struct BIT {
int t[MaxN];
inline int lowbit(int i) {
return i & -i;
}
inline void clear(int x) {
for (int i = x; i <= N; i += lowbit(i))
t[i] = 0;
}
inline void update(int x, int v) {
for (int i = x; i <= N; i += lowbit(i))
t[i] = std::max(t[i], v);
}
inline int query(int x) {
int res = 0;
for (int i = x; i > 0; i -= lowbit(i))
res = std::max(res, t[i]);
return res;
}
} T;
inline bool cmp(int x, int y) {
return A[x] < A[y];
}
void cdq(int l, int r) {
if (l == r) {
dp[l] = std::max(dp[l], C[l]);
return;
}
int mid = (l + r) >> 1;
cdq(l, mid);
for (int i = l; i <= r; ++i) lnk[i] = i;
std::sort(lnk + l, lnk + 1 + mid, cmp); std::sort(lnk + 1 + mid, lnk + 1 + r, cmp);
for (int i = mid + 1, j = l; i <= r; ++i) {
while (j <= mid && A[lnk[j]] <= A[lnk[i]]) {
T.update(B[lnk[j]], dp[lnk[j]]);
j++;
}
dp[lnk[i]] = std::max(dp[lnk[i]], T.query(B[lnk[i]]) + C[lnk[i]]);
}
for (int i = l; i <= mid; ++i) T.clear(B[lnk[i]]);
cdq(mid + 1, r);
}
void solve() {
std::reverse(A + 1, A + 1 + N);
std::reverse(B + 1, B + 1 + N);
std::reverse(C + 1, C + 1 + N);
cdq(1, N);
int ans = 0;
for (int i = 1; i <= N; ++i)
ans = std::max(ans, dp[i]);
printf("%d\n", ans);
}
}
void solve() {
if (N > 1000 && isChain == true) Subtask2::solve();
else Subtask1::solve();
}
int main() {
init();
solve();
return 0;
}
看了一下别人的代码,发现正解是树套树。
本题要求的就是一条端点为根的链,要求选取这条链上的若干点,使得满足这些点按照深度从浅到深的顺序,\(A, B\) 序列单调不升,且权值和 \(C\) 最大。
树上的 DFS 有一个性质:搜索到一个点 \(u\) 时,在递归栈中的所有点,恰好是 \(u\) 到根节点简单路径上的所有点,也就是 \(u\) 号点的所有祖先。
于是从上往下进行动态规划。\(f_i\) 表示根节点到 \(i\) 的这条链上,强制选了 \(i\) 号点,最大的价值和。
然后每次递归到 \(u\) 时,往二维平面内加入一个 \((A_u, B_u)\),权值为 \(f_u\) 的点。\(f_u\) 的确定也很简单,用树套树找出左下角为 \((A_u, B_u)\),右上角为 \((+\infty, +\infty)\) 这个矩形内权值最大的点即可。
具体实现可以离散完然后使用可持久化线段树套动态开点线段树维护加入删除和求区间最大值。
时间复杂度 \(\mathcal{O}(n \log^2 n)\),常数不是太大的话应该能过得去。
代码:(这份代码常数比较大,会被第 \(5\) 个点卡。与上面暴力代码中的 \(\texttt{Subtask #2}\) 的链上直接 CDQ 结合一下即可 AC,这份代码太丑了就不贴在博客里了。)
#include <algorithm>
#include <cstdio>
const int MaxN = 100000 + 5;
const int MaxM = 2000000 + 5, MaxV = 40000000 + 5;
struct Graph {
int cnte;
int Head[MaxN], To[MaxN], Next[MaxN];
inline void add_edge(int from, int to) {
cnte++; To[cnte] = to;
Next[cnte] = Head[from]; Head[from] = cnte;
}
};
int N;
int A[MaxN], B[MaxN], C[MaxN];
int Da[MaxN], Db[MaxN];
int dp[MaxN];
Graph G;
struct dynamicSegTree {
int rt[MaxM], cntv;
int lson[MaxV], rson[MaxV];
int Mx[MaxV];
inline void pushup(int i) { Mx[i] = std::max(Mx[lson[i]], Mx[rson[i]]); }
void Update_Tree(int pos, int val, int &id, int baseid, int l = 1, int r = N) {
if (id == 0) id = ++cntv;
if (l == r) {
Mx[id] = std::max(Mx[baseid], val);
return;
}
int mid = (l + r) >> 1;
if (pos <= mid) {
Update_Tree(pos, val, lson[id], lson[baseid], l, mid);
rson[id] = rson[baseid];
} else {
Update_Tree(pos, val, rson[id], rson[baseid], mid + 1, r);
lson[id] = lson[baseid];
}
pushup(id);
}
int Query_Tree(int left, int right, int id, int l = 1, int r = N) {
if (left == l && right == r) return Mx[id];
int mid = (l + r) >> 1;
if (right <= mid) return Query_Tree(left, right, lson[id], l, mid);
else if (left > mid) return Query_Tree(left, right, rson[id], mid + 1, r);
else return std::max(Query_Tree(left, mid, lson[id], l, mid), Query_Tree(mid + 1, right, rson[id], mid + 1, r));
}
} P;
struct perSegTree {
int rt[MaxN], cntv;
int lson[MaxM], rson[MaxM];
void Update_Tree(int x, int y, int val, int &id, int base, int l = 1, int r = N) {
if (id == 0) id = ++cntv;
P.Update_Tree(y, val, P.rt[id], P.rt[base]);
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid) {
Update_Tree(x, y, val, lson[id], lson[base], l, mid);
rson[id] = rson[base];
} else {
Update_Tree(x, y, val, rson[id], rson[base], mid + 1, r);
lson[id] = lson[base];
}
}
int Query_Tree(int xl, int xr, int yl, int yr, int id, int l = 1, int r = N) {
if (xl == l && xr == r) return P.Query_Tree(yl, yr, P.rt[id]);
int mid = (l + r) >> 1;
if (xr <= mid) return Query_Tree(xl, xr, yl, yr, lson[id], l, mid);
else if (xl > mid) return Query_Tree(xl, xr, yl, yr, rson[id], mid + 1, r);
else return std::max(Query_Tree(xl, mid, yl, yr, lson[id], l, mid), Query_Tree(mid + 1, xr, yl, yr, rson[id], mid + 1, r));
}
} T;
void dfs(int u, int f) {
dp[u] = T.Query_Tree(1, A[u], 1, B[u], T.rt[f]) + C[u];
T.Update_Tree(A[u], B[u], dp[u], T.rt[u], T.rt[f]);
for (int i = G.Head[u]; i; i = G.Next[i]) {
int v = G.To[i];
dfs(v, u);
}
}
void init() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) {
scanf("%d %d %d", &A[i], &B[i], &C[i]);
Da[i] = A[i], Db[i] = B[i];
}
std::sort(Da + 1, Da + 1 + N); std::sort(Db + 1, Db + 1 + N);
for (int i = 1; i <= N; ++i) {
A[i] = std::lower_bound(Da + 1, Da + 1 + N, A[i]) - Da;
B[i] = std::lower_bound(Db + 1, Db + 1 + N, B[i]) - Db;
A[i] = N - A[i] + 1;
B[i] = N - B[i] + 1;
}
for (int i = 1; i < N; ++i) {
int u, v;
scanf("%d %d", &u, &v);
G.add_edge(u, v);
}
}
void solve() {
dfs(1, 0);
int ans = 0;
for (int i = 1; i <= N; ++i) ans = std::max(ans, dp[i]);
printf("%d\n", ans);
}
int main() {
init();
solve();
return 0;
}
\[ \texttt{by Tweetuzki}\ \ \ \ \ \mathcal{2019.04.27} \]