2018冬令营模拟测试赛(三)
[Problem A]摧毁图状树
试题描述
输入
见“试题描述”
输出
见“试题描述”
输入示例
见“试题描述”
输出示例
见“试题描述”
数据规模及约定
见“试题描述”
题解
这题没想到贪心 QwQ,那就没戏了……
贪心就是每次选择一个最深的且没有被覆盖的点向上覆盖 \(k\) 层,因为这个“最深的没有被覆盖的点”不可能再有其它点引出的链覆盖它了,而它又不得不被覆盖,那么向上引出的链自然越长越好。
那么接下来就是如何用数据结构快速模拟这个过程。
我们考虑处理出 \(k = 1 \sim \mathrm{max}\{dep_i\}\)(即最大深度)的所有答案,我们需要努力构造方法使得对于一个 \(k\) 我们只会访问部分节点,而不是全部。考虑到叶子节点很特殊,怎么都需要算上它们,但每次遍历一遍所有叶子又非常费时,所以我们可以一开始从“最近的叶子距离等于 \(k\)”的节点出发,以深度为关键字放在堆里,每次拿出一个节点,如果它没有被覆盖,就往上跳 \(k\) 级祖先并把它加到堆里。
我们可以用线段树 + DFS 序查询每个节点是否被覆盖(一个节点被覆盖当且仅当它子树中被拿出来过的节点的深度和它的深度之差小于 \(k\)),倍增实现跳 \(k\) 级祖先,于是所有操作都是一个 \(\mathrm{log}n\)。
考虑堆里的节点个数。下面先不考虑叶子节点,因为我们事先把它们踢出去了,事后加上叶子个数就好了。先从树上删掉所有被我们选中的节点,那么剩下的每个连通块大小不会小于 \(k-1\),再把那些点加回去,那么每个连通块大小不超过 \(k\),而每个被选中的节点顶多提供一个不会被选中的 \(k\) 级祖先。所以算法遍历到的点数不超过 \(\sum_{k=1}^{\mathrm{max}\{dep_i\}} {\frac{(n-L)}{k}}\) 显然这是调和级数,可以把它看成 \(n \mathrm{log} n\)。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
#define rep(i, s, t) for(int i = (s); i <= (t); i++)
#define dwn(i, s, t) for(int i = (s); i >= (t); i--)
int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)){ if(c == '-') f = -1; c = getchar(); }
while(isdigit(c)){ x = x * 10 + c - '0'; c = getchar(); }
return x * f;
}
#define maxn 100010
#define maxm 200010
#define maxlog 17
#define oo 2147483647
int n, m, head[maxn], nxt[maxm], to[maxm];
void AddEdge(int a, int b) {
to[++m] = b; nxt[m] = head[a]; head[a] = m;
swap(a, b);
to[++m] = b; nxt[m] = head[a]; head[a] = m;
return ;
}
int q, que[maxn], cq, task[maxn], Ans[maxn];
int nearest[maxn], dep[maxn], mxd, clo, dl[maxn], dr[maxn], uid[maxn], fa[maxn][maxlog], cntl;
vector <int> Kfar[maxn];
void build(int u) {
uid[dl[u] = ++clo] = u;
rep(i, 1, maxlog - 1) fa[u][i] = fa[fa[u][i-1]][i-1];
nearest[u] = n + 1;
mxd = max(mxd, dep[u]);
for(int e = head[u]; e; e = nxt[e]) if(to[e] != fa[u][0]) {
fa[to[e]][0] = u;
dep[to[e]] = dep[u] + 1;
build(to[e]);
nearest[u] = min(nearest[u], nearest[to[e]] + 1);
}
if(nearest[u] == n + 1) nearest[u] = 0, cntl++;
dr[u] = clo;
return ;
}
int KthParent(int u, int k) {
rep(i, 0, maxlog - 1) if(k >> i & 1) u = fa[u][i];
return u ? u : 1;
}
struct Seg {
int minv[maxn<<2];
Seg() { rep(i, 1, (maxn << 2) - 1) minv[i] = oo; }
void modify(int o, int l, int r, int p, int tp) { // tp == 1 means add, tp == -1 means delete.
if(l == r) {
if(tp > 0) minv[o] = dep[uid[l]];
else minv[o] = oo;
return ;
}
int mid = l + r >> 1, lc = o << 1, rc = lc | 1;
if(p <= mid) modify(lc, l, mid, p, tp);
else modify(rc, mid + 1, r, p, tp);
minv[o] = min(minv[lc], minv[rc]);
return ;
}
int query(int o, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) return minv[o];
int mid = l + r >> 1, lc = o << 1, rc = lc | 1, ans = oo;
if(ql <= mid) ans = min(ans, query(lc, l, mid, ql, qr));
if(qr > mid) ans = min(ans, query(rc, mid + 1, r, ql, qr));
return ans;
}
} sg;
struct cmp {
bool operator () (const int& a, const int& b) const {
return dep[a] < dep[b];
}
};
priority_queue <int, vector <int>, cmp> Q;
int grasp[maxn], cntg;
void solve(int k) {
rep(i, 0, (int)Kfar[k].size() - 1) Q.push(Kfar[k][i]);
while(!Q.empty()) {
int u = Q.top(); Q.pop();
if(sg.query(1, 1, n, dl[u], dr[u]) < dep[u] + k || nearest[u] < k) continue;
grasp[++cntg] = u;
sg.modify(1, 1, n, dl[u], 1);
Q.push(KthParent(u, k));
}
Ans[k] = cntg + cntl;
while(cntg) sg.modify(1, 1, n, dl[grasp[cntg--]], -1);
return ;
}
int main() {
n = read();
rep(i, 1, n - 1) {
int a = read(), b = read();
AddEdge(a, b);
}
build(1);
q = read();
rep(i, 1, q) {
que[i] = read();
task[i] = que[i] = que[i] <= mxd ? que[i] : mxd;
}
sort(task + 1, task + q + 1);
cq = unique(task + 1, task + q + 1) - task - 1;
rep(i, 1, n) Kfar[nearest[i]].push_back(i);
rep(i, 1, cq) solve(task[i]);
rep(i, 1, q) printf("%d\n", Ans[que[i]]);
return 0;
}
[Problem B]排列统计
试题描述
输入
见“试题描述”
输出
见“试题描述”
输入示例
见“试题描述”
输出示例
见“试题描述”
数据规模及约定
见“试题描述”
题解
妈妈呀这题 dp 好神!思路很棒。
引用一下原题解的话“我们想进行 dp,必须设法避免指数级的状态设计。为了做到这一点,我们尝试枚举计算每个值对答案的贡献、每个位置对答案的贡献,发现怎么都避免不了指数级状态的支配。考虑换一种思路——枚举每个值在每个位置对答案产生的贡献!”
这样似乎“枚举”次数多了,也即进行 dp 的轮数多了,但是我们却可以把状态降成多项式级别的,神奇吧!
设 \(f(i, j, p, c)\) 表示进行了 \(i\) 轮交换;在目标位置前的比目标数字大的数的个数为 \(j\);目标数字在目标位置左边时 \(p = 0\),在目标位置上时 \(p = 1\),在目标位置右边时 \(p = 2\);目标位置上的数小于目标数字时 \(c = 0\),等于目标数字时 \(c = 1\),大于目标数字时 \(c = 2\);在这种情况下的方案数。转移就是枚举每种数字之间的交换(即目标位置左边比目标数字小的数、左边大数、目标位置上的数、右边小数、右边大数、目标数字,详见代码注释)。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
#define rep(i, s, t) for(int i = (s); i <= (t); i++)
#define dwn(i, s, t) for(int i = (s); i >= (t); i--)
int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)){ if(c == '-') f = -1; c = getchar(); }
while(isdigit(c)){ x = x * 10 + c - '0'; c = getchar(); }
return x * f;
}
#define maxn 110
#define MOD 1000000007
#define LL long long
int n, K, A[maxn], f[maxn][maxn][3][3], ans;
void upd(int& a, LL b) {
a = (b + a) % MOD;
return ;
}
int Pow(int a, int b) {
int ans = 1, t = a;
while(b) {
if(b & 1) ans = (LL)ans * t % MOD;
t = (LL)t * t % MOD; b >>= 1;
}
return ans;
}
int main() {
n = read(); K = read();
rep(i, 1, n) A[i] = read();
rep(val, 1, n) rep(pos, 1, val) {
int _j = 0, _p, _c;
rep(i, 1, pos - 1) if(A[i] > val) _j++;
rep(i, 1, n) if(A[i] == val) {
if(i < pos) _p = 0;
if(i == pos) _p = 1;
if(i > pos) _p = 2;
}
if(A[pos] < val) _c = 0;
if(A[pos] == val) _c = 1;
if(A[pos] > val) _c = 2;
memset(f, 0, sizeof(f));
f[0][_j][_p][_c] = 1;
// printf("start(%d, %d):\n", val, pos);
rep(i, 0, K - 1) rep(j, 0, min(n - val, pos - 1)) rep(p, 0, 2) rep(c, 0, 2) if(f[i][j][p][c]) {
int now = f[i][j][p][c], ls = pos - 1 - j - (p == 0), ll = j, rs = val - 1 - ls - (c == 0), rl = n - val - ll - (c == 2);
/*printf("f[%d][%d][%d][%d] = %d | %d %d %d %d\n", i, j, p, c, f[i][j][p][c], ls, ll, rs, rl);
if(i == K) continue; // */
// symbol <> means "swap with".
// left smaller <> left smaller
if(ls) upd(f[i+1][j][p][c], (LL)now * ls * ls);
// left smaller <> left larger
if(ls && ll) upd(f[i+1][j][p][c], (LL)now * ls * ll * 2);
// left smaller <> number on position
if(ls) upd(f[i+1][j+(c==2)][p==1?0:p][0], (LL)now * ls * 2);
// left smaller <> right smaller
if(ls && rs) upd(f[i+1][j][p][c], (LL)now * ls * rs * 2);
// left smaller <> right larger
if(ls && rl) upd(f[i+1][j+1][p][c], (LL)now * ls * rl * 2);
// left larger <> left larger
if(ll) upd(f[i+1][j][p][c], (LL)now * ll * ll);
// left larger <> number on position
if(ll) upd(f[i+1][j-(c!=2)][p==1?0:p][2], (LL)now * ll * 2);
// left larger <> right smaller
if(ll && rs) upd(f[i+1][j-1][p][c], (LL)now * ll * rs * 2);
// left larger <> right larger
if(ll && rl) upd(f[i+1][j][p][c], (LL)now * ll * rl * 2);
// number on position <> number on position
upd(f[i+1][j][p][c], now);
// number on position <> right smaller
if(rs) upd(f[i+1][j][p==1?2:p][0], (LL)now * rs * 2);
// number on position <> right larger
if(rl) upd(f[i+1][j][p==1?2:p][2], (LL)now * rl * 2);
// right smaller <> right smaller
if(rs) upd(f[i+1][j][p][c], (LL)now * rs * rs);
// right smaller <> right larger
if(rs && rl) upd(f[i+1][j][p][c], (LL)now * rs * rl * 2);
// right larger <> right larger
if(rl) upd(f[i+1][j][p][c], (LL)now * rl * rl);
if(p != 1 && c != 1) { // (ONLY when y is not on position)
// val y <> left smaller
if(ls) upd(f[i+1][j][0][c==1?0:c], (LL)now * ls * 2);
// val y <> left larger
if(ll) upd(f[i+1][j-(p!=0)][0][c==1?2:c], (LL)now * ll * 2);
// val y <> number on position
upd(f[i+1][j+(p==0&&c==2)][1][1], 2ll * now);
// val y <> right smaller
if(rs) upd(f[i+1][j][2][c==1?0:c], (LL)now * rs * 2);
// val y <> right larger
if(rl) upd(f[i+1][j+(p==0)][2][c==1?2:c], (LL)now * rl * 2);
// val y <> val y
upd(f[i+1][j][p][c], now);
}
}
upd(ans, f[K][0][1][1]);
// printf("add: %d\n", f[K][0][1][1]);
}
ans = (LL)ans * Pow(Pow(n, K << 1), MOD - 2) % MOD;
printf("%d\n", ans);
return 0;
}
[Problem C]归并排序
试题描述
输入
见“试题描述”
输出
见“试题描述”
输入示例
见“试题描述”
输出示例
见“试题描述”
数据规模及约定
见“试题描述”
题解
首先这题需要观察出一些性质:我们把归并排序最后可能排错的两个数看成一对数,对于一对数 \((a, b)\),不妨令 \(a < b\),因为大小关系是等概率出现的;那么当这对数经过归并排序之后的顺序是先 \(a\) 后 \(b\),它们就有可能离得很远,但一定会保证 \(b\) 每时每刻都在 \(a\) 的右边;如果顺序是先 \(b\) 后 \(a\),我们会发现,在每次归并的时候,如果 \(b\) 能进入序列(这时一定是另一半轮到了一个大于 \(b\) 的数 \(c\)),那么紧随其后的 \(a\) 更能进入序列,因为 \(a < b < c\)。
令 \(a_i(i \in [1, n])\) 表示排序前的排列。
接下来对于每个询问,假设询问的是位置 \(x\) 到位置 \(y\),我们看两个数都比 \(a_x\) 小的数对(也即最大值比 \(a_x\) 小的数对),和两个数都比 \(a_x\) 大的数对(也即最小值比 \(a_x\) 大的数对),剩下的就是两个数“跨过”\(a_x\) 的数对。不难发现都比 \(a_x\) 小的数对肯定整对都在 \(a_x\) 的前面,都比 \(a_x\) 大的数对肯定都在 \(a_x\) 的后面,“跨过”\(a_x\) 的数对有 \(\frac{1}{2}\) 的概率有一个数在 \(a_x\) 前面,\(\frac{1}{2}\) 的概率都在 \(a_x\) 的后面,这样的话,组合数算一下方案数再除以 \(2\) 的幂就好了。
注意 \(a_x\) 所在对交换可能会导致情况不一样,记得讨论。
统计那个东西开两个树状数组分别维护数对中较小值和较大值就好了。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
#define rep(i, s, t) for(int i = (s); i <= (t); i++)
#define dwn(i, s, t) for(int i = (s); i >= (t); i--)
int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)){ if(c == '-') f = -1; c = getchar(); }
while(isdigit(c)){ x = x * 10 + c - '0'; c = getchar(); }
return x * f;
}
#define maxn 70000
#define MOD 1000000007
#define LL long long
#define pii pair <int, int>
#define x first
#define y second
#define mp(x, y) make_pair(x, y)
int n, bel[maxn], Arr[maxn];
pii A[maxn];
struct BitArray {
int C[maxn];
void upd(int x, int v) {
for(; x <= n; x += x & -x) C[x] += v;
return ;
}
int sum(int x) {
int ans = 0;
for(; x; x -= x & -x) ans += C[x];
return ans;
}
} mx, mn;
int Pow(int a, int b) {
int ans = 1, t = a;
while(b) {
if(b & 1) ans = (LL)ans * t % MOD;
t = (LL)t * t % MOD; b >>= 1;
}
return ans;
}
int fac[maxn], ifac[maxn];
int calc(int n, int m) { return (LL)fac[n] * ifac[m] % MOD * ifac[n-m] % MOD; }
int main() {
n = read();
for(int i = 1; i <= n; i += 2) {
bel[i] = bel[i+1] = (i - 1 >> 1) + 1;
// printf("%d %d %d %d\n", i, i + 1, bel[i], bel[i+1]);
int x = read(), y = read();
Arr[i] = x; Arr[i+1] = y;
if(x > y) swap(x, y);
A[bel[i]] = mp(x, y);
mx.upd(y, 1); mn.upd(x, 1);
}
fac[0] = ifac[0] = 1;
rep(i, 1, n) fac[i] = (LL)fac[i-1] * i % MOD, ifac[i] = (LL)ifac[i-1] * Pow(i, MOD - 2) % MOD;
int q = read();
while(q--) {
int tp = read(), x = read(), y = read();
if(tp == 1) {
swap(Arr[x], Arr[y]);
if(bel[x] == bel[y]) continue;
mx.upd(A[bel[x]].y, -1); mn.upd(A[bel[x]].x, -1);
mx.upd(A[bel[y]].y, -1); mn.upd(A[bel[y]].x, -1);
A[bel[x]] = mp(min(Arr[(bel[x]<<1)-1], Arr[bel[x]<<1]), max(Arr[(bel[x]<<1)-1], Arr[bel[x]<<1]));
A[bel[y]] = mp(min(Arr[(bel[y]<<1)-1], Arr[bel[y]<<1]), max(Arr[(bel[y]<<1)-1], Arr[bel[y]<<1]));
// printf("%d %d %d | %d %d %d\n", bel[x], A[bel[x]].x, A[bel[x]].y, bel[y], A[bel[y]].x, A[bel[y]].y);
mx.upd(A[bel[x]].y, 1); mn.upd(A[bel[x]].x, 1);
mx.upd(A[bel[y]].y, 1); mn.upd(A[bel[y]].x, 1);
}
if(tp == 2) {
int alls, allb, rest, ans = 0;
if(Arr[x] == A[bel[x]].y) {
alls = mx.sum(Arr[x] - 1); allb = mn.sum(n) - mn.sum(Arr[x]); rest = (n >> 1) - alls - allb;
if((alls << 1) > y - 1 || (alls << 1) + rest < y - 1) puts("0");
else printf("%lld\n", (LL)calc(rest, y - 1 - (alls << 1)) * Pow(Pow(2, rest), MOD - 2) % MOD);
continue;
}
alls = mx.sum(A[bel[x]].x - 1); allb = mn.sum(n) - mn.sum(A[bel[x]].x); rest = (n >> 1) - alls - allb - 1;
if((alls << 1) > y - 1 || (alls << 1) + rest < y - 1) ;
else ans += (LL)calc(rest, y - 1 - (alls << 1)) * Pow(Pow(2, rest + 1), MOD - 2) % MOD;
alls = mx.sum(A[bel[x]].y - 1); allb = mn.sum(n) - mn.sum(A[bel[x]].y); rest = (n >> 1) - alls - allb - 1;
if((alls << 1) > y - 2 || (alls << 1) + rest < y - 2) ;
else (ans += (LL)calc(rest, y - 2 - (alls << 1)) * Pow(Pow(2, rest + 1), MOD - 2) % MOD) %= MOD;
printf("%d\n", ans);
}
}
return 0;
}