【总结】JOISC 2019
作者:互联网
「JOISC 2019 Day1」考试
三维偏序模板,直接跑 cdq 分治。
#define N 200005
int n, m, b[N], t, ed[N], c[N];
inline void add(int x,int y){for(; x <= t; x += x & -x)c[x] += y;}
inline int ask(int x){int sum = 0; for(; x; x -= x & -x)sum += c[x]; return sum;}
struct node{
int op, x, y, z;
bool operator<(const node o)const{
if(x != o.x)return x < o.x;
return op > o.op;
}
}a[N], u[N];
void solve(int l,int r){
if(l == r)return;
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
int j = mid + 1, s = 0;
rep(i, l, mid){
while(j <= r && a[j].y >= a[i].y){
if(!a[j].op)add(a[j].z, 1);
u[++s] = a[j++];
}
u[++s] = a[i];
if(a[i].op)ed[a[i].op] += ask(t) - ask(a[i].z - 1);
}
rep(i, mid + 1, j - 1)if(!a[i].op)add(a[i].z, -1);
while(j <= r)u[++s] = a[j++];
rp(i, s)a[l + i - 1] = u[i];
}
int main() {
read(n, m);
rp(i, n)read(a[i].x, a[i].y), b[i] = a[i].z = a[i].x + a[i].y;
sort(b + 1, b + n + 1), t = unique(b + 1, b + n + 1) - b - 1;
rp(i, n)a[i].z = lower_bound(b + 1, b + t + 1, a[i].z) - b;
rp(i, m)read(a[i + n].x, a[i + n].y, a[i + n].z), a[i + n].op = i,
a[i + n].z = lower_bound(b + 1, b + t + 1, a[i + n].z) - b;
sort(a + 1, a + n + m + 1);
solve(1, n + m);
rp(i, m)printf("%d\n", ed[i]);
return 0;
}
「JOISC 2019 Day1」聚会
交互题,给定一棵树,每次可以询问 \((x,y,z)\),返回到三点距离之和最短的点,需要在有限次数内还原这棵树。
这题看上去就不大正经,考虑乱搞,比如随机化。
我们随机一个点为根,每次询问可以知道两个点是否在一棵子树,然后分治下去即可。但是这个做法效率很低,只有 \(17\) 分。
观察一下,发现我们询问的时候有很多情况是两个点不在一棵子树,这样的询问相当于浪费了。所以我们随机两个点 \(x, y\) 出来,然后对于其余的点依次进行一次询问,就可以知道每个点是否在 \(x,y\) 之间的链上,如果不在,可以得到是接在链上的哪个点上。这样也可以递归下去,效率足够通过这题。
mt19937 rd(time(0));
void link(int x,int y){
if(x > y)swap(x, y);
Bridge(x, y);
}
void solve(vector<int>a){
if(si(a) == 1)return;
if(si(a) == 2){link(a[0], a[1]); return;}
shuffle(a.begin(), a.end(), rd);
int x = a[0], y = a[1], s = si(a) - 1;
vector<Pr>u; vector<int> p;
u.pb(mp(x, x)), u.pb(mp(y, y)),
p.pb(x), p.pb(y);
rep(i, 2, s){
int z = Query(x, y, a[i]);
u.pb(mp(z, a[i]));
if(z == a[i])p.pb(z);
}
sort(u.begin(), u.end());
for(int i = 0; i <= s; ){
int j = i;
while(j <= s && u[j].fi == u[i].fi)j++;
vector<int>c;
rep(k, i, j - 1)c.pb(u[k].se);
solve(c), i = j;
}
solve(p);
}
void Solve(int n){
vector<int>a;
rp(i, n)a.pb(i - 1);
solve(a);
}
「JOISC 2019 Day1」馕
给定长度为 \(M\) 的馕,和 \(N\) 个人,第 \(i\) 个人吃馕的第 \(j\) 米可以获得 \(V_i,j\) 的收益,现在需要将长为 \(M\) 的馕分成 \(N\) 段,每个人吃一段,使得每个人获得的收益至少为这个人吃掉整个馕的收益的 \(\frac{1}{N}\),切的位置可以是分数。
这题面首先看起来就非常没有头绪,我们可以枚举一下几种可能的思路。
一个比较显然的方向是,如果依次考虑每个人,那么每个人肯定恰好吃到他总收益的 \(\frac{1}{N}\),这样对后面的人分配会更优。
然后是人类智慧的构造,我们考虑每个人的 \(N\) 等分点,使得每一段恰好是他总收益的 \(\frac{1}{N}\)。那么贪心取得第一段是所有人中 \(N\) 等分最短的一段。那么这个人被满足了,直接删去,归纳下去即可得到正确答案。
#define N 2005
int n, m, a[N][N], c[N], v[N];
struct node{
int x, y;
bool operator<(const node o)const{ return x * (__int128)o.y < o.x * (__int128)y; }
bool operator>=(const node o)const{ return x * (__int128)o.y >= o.x * (__int128)y;}
}u[N][N];
signed main() {
read(n, m);
rp(i, n)rp(j, m)read(a[i][j]);
rp(id, n){
int j = 1, sum = 0, cur = 0;
rp(i, m)sum += a[id][i];
rp(i, m){
while(j <= n && node{cur + a[id][i], 1} >= node{sum * j, n})
u[id][j] = node{sum * j - cur * n + n * a[id][i] * (i - 1), n * a[id][i]}, j++;
if(j > n)break;
cur += a[id][i];
}
}
rp(i, n){
node cur{1, 0}; int w = 0;
rp(j, n)if(!v[j] && u[j][i] < cur)cur = u[j][i], w = j;
v[w] = 1, c[i] = w;
if(i != n)printf("%lld %lld\n", cur.x, cur.y);
}
rp(i, n)printf("%lld ", c[i]); el;
return 0;
}
「JOISC 2019 Day2」两个天线
\(N\) 个天线,天线高度 \(H_i\),天线 \(i\) 可以向到它距离在 \([A_i,B_i]\) 范围内的天线发送信息。多次询问区间 \([L_j,R_j]\) 内可以相互发送信息的天线的 \(|H_x- H_y|\) 的最大值。
问题是静态的,考虑离线,将所有询问按 \(R\) 排序。
我们要求 \(|H_x - H_y|\) 的最大值,可以直接去绝对值求 \(H_x - H_y\) 和 \(H_y - H_x\) 的最大值,不失一般性我们令 \(x < y\)。
那么随着询问 \(R\) 的增大,相当于每次新增一个 \(y\),将可行的 \(x\) 加入答案集合,那么 \(L\) 的限制可以通过 DS 比如线段树维护。
对于 \(y\),可能合法的 \(x\) 区间是 \([y - B_y, y - A_y]\),但是条件不充分,对于 \(x\) 还要满足 \(y \in [x + A_x, x + B_x]\),简单思考后发现 \(x\) 的限制可以通过扫描线维护。
那么我们线段树需要支持维护 \(F,G\) 序列:
1.修改 \(F_x\) 的值
2.区间操作 \((l,r,w)\),\(\forall i \in [l,r]\),\(G_i \leftarrow F_i + w\)。
3.区间查询最大值。
那么线段树维护区间 \(F,G\) 的最大值,和区间操作最大值的标记即可,时间复杂的 \(\mathcal{O}(N\log N)\)。
#define N 200005
int n, m, h[N], l[N], r[N], ed[N];
struct node{
int l, r, id;
bool operator<(const node o)const{return r < o.r;}
}q[N];
struct Node{int l, r, w, val, tag;}a[N << 2];
#define L a[x].l
#define R a[x].r
#define ls (x << 1)
#define rs (ls | 1)
#define S a[x].val
#define W a[x].w
#define T a[x].tag
void build(int x,int l,int r){
L = l, R = r, T = S = W = inf_;
if(l == r)return ;
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
}
void pushup(int x,int val){cmx(W, S + val), cmx(T, val);}
void down(int x){if(T != inf_)pushup(ls, T), pushup(rs, T), T = inf_;}
void ins(int x,int pos,int op){
if(L == R)S = (op ? op * h[L] : inf_);
else{
down(x); int mid = (L + R) >> 1;
if(mid >= pos)ins(ls, pos, op);
else ins(rs, pos, op);
S = max(a[ls].val, a[rs].val);
}
}
void solve(int x,int l,int r,int val){
if(L >= l && R <= r)pushup(x, val);
else{
down(x); int mid = (L + R) >> 1;
if(mid >= l)solve(ls, l, r, val);
if(mid < r)solve(rs, l, r, val);
W = max(a[ls].w, a[rs].w);
}
}
int ask(int x,int l,int r){
if(L >= l && R <= r)return W;
down(x); int mid = (L + R) >> 1;
if(mid >= l)return mid < r ? max(ask(ls, l, r), ask(rs, l, r)) : ask(ls, l, r);
return ask(rs, l, r);
}
vector<int>c[N], d[N];
signed main() {
read(n), memset(ed, ~0, sizeof(ed));
rp(i, n)read(h[i], l[i], r[i]);
read(m);
rp(i, m)read(q[i].l, q[i].r), q[i].id = i;
sort(q + 1, q + m + 1);
build(1, 1, n);
int j = 1;
rp(i, n){
go(x, c[i])ins(1, x, -1);
go(x, d[i])ins(1, x, 0);
int ll = max(1LL, i - r[i]), rr = i - l[i];
if(ll <= rr)solve(1, ll, rr, h[i]);
while(j <= m && q[j].r == i)cmx(ed[q[j].id], ask(1, q[j].l, i)), j++;
ll = i + l[i], rr = min(n, i + r[i]);
if(ll <= rr)c[ll].pb(i), d[rr + 1].pb(i);
}
build(1, 1, n), j = 1;
rp(i, n)c[i].clear(), d[i].clear();
rp(i, n){
go(x, c[i])ins(1, x, 1);
go(x, d[i])ins(1, x, 0);
int ll = max(1LL, i - r[i]), rr = i - l[i];
if(ll <= rr)solve(1, ll, rr, -h[i]);
while(j <= m && q[j].r == i)cmx(ed[q[j].id], ask(1, q[j].l, i)), j++;
ll = i + l[i], rr = min(n, i + r[i]);
if(ll <= rr)c[ll].pb(i), d[rr + 1].pb(i);
}
rp(i, m)printf("%lld\n", ed[i]);
return 0;
}
「JOISC 2019 Day2」两道料理
两道料理分别要 \(n,m\) 个操作,每个操作需要 \(t_i\) 的时间,如果它在 \(p_i\) 之前完成,就能获得 \(w_i\) 的收益。两个料理的操作顺序已经给定,你需要归并两道料理的操作,使得总收益最大。
我们求出 \(p_i\) 表示第一道料理第 \(i\) 个操作前面最多可以进行第二道料理的前 \(p_i\) 的操作,同理对于第二道料理求出 \(q_j\),那么我们可以直接二维 DP 求出答案其中 \(f_{i,j}\) 表示两道料理分别进行了 \(i,j\) 步的最大收益。
由于方程是 \(f_{i,j} \leftarrow f_{i,j - 1},f_{i-1,j}\),每次是 \(i/j\) 增加 \(1\),让人联想到走格子的过程,DP 的过程就是从 \((0,0) \to (n,m)\) 的过程。
所以将问题进行转化,如果将 \((i,p_i)\) 看成点,那么有收益当且仅当点在路径上方,同理对于 \((q_j,j)\),就在路径下方。经典操作,我们加上所有 \((i,p_i)\) 的权值,然后将其权值取反,适当调整就转化为路径下方,求出前缀和 \(s_{i,j}\) 表示点 \((i,j)\) 下面的点权和,有新的转移方程 \(f_{i,j} = \max\{f_{i - 1, j} + s_{i - 1, j},f_{i, j - 1}\}\)。
这个形式就非常优美了,转移过程相当于将 \(f_{i - 1}\) 加上 \(s_{i - 1}\) 得到 \(f_i\),然后对 \(f_i\) 取前缀最大值。这样我们维护 \(f\) 的差分数组,\(s\) 的差分数组就是点权,所以 \(s\) 中有值的就 \(n + m\) 个。取最大值可以将差分数组中 \(<0\) 的位置加到后面,然后删除该位置。用 set/map 维护是均摊 $(n + m)\log $ 的。
#define N 1000005
int n, m; LL ans;//, s[N][N];
struct node{LL t, p, w;}a[N], b[N];
vector<Pr>c[N]; set<int>s;
int main() {
read(n, m);
rp(i, n)read(a[i].t, a[i].p, a[i].w), a[i].t += a[i - 1].t;
rp(i, m)read(b[i].t, b[i].p, b[i].w), b[i].t += b[i - 1].t;
rp(i, n){
LL t = a[i].p - a[i].t;
int l = 0, r = m, j = ~0;
while(l <= r){
int mid = (l + r) >> 1;
if(b[mid].t <= t)j = mid, l = mid + 1;
else r = mid - 1;
}
ans += a[i].w;
if(j + 1 <= m)c[i - 1].pb(mp(j + 1, -a[i].w));
}
rp(i, m){
LL t = b[i].p - b[i].t;
int l = 0, r = n, j = ~0;
while(l <= r){
int mid = (l + r) >> 1;
if(a[mid].t <= t)j = mid, l = mid + 1;
else r = mid - 1;
}
if(j >= 0)c[j].pb(mp(i, b[i].w));
}
map<int,LL>f; LL st = 0;
rp(i, n){
go(x, c[i - 1]){
if(!x.fi)st += x.se;
else {
f[x.fi] += x.se;
if(f[x.fi] < 0)s.insert(x.fi);
}
}
while(!s.empty()){
int x = *s.begin(); s.erase(x);
if(f.count(x) && f[x] < 0){
LL w = f[x]; f.erase(x);
auto y = f.lower_bound(x);
if(y != f.end()){
int z = (*y).fi;
f[z] += w;
if(f[z] < 0)s.insert(z);
}
}
}
}
go(x, f)ans += x.se;
go(x, c[n])ans += x.se;
printf("%lld\n", ans + st);
return 0;
}
「JOISC 2019 Day3」指定城市
给定一棵树,双向边,每条边两个方向的权值分别为 \(C_i, D_i\),多次询问 \(k\),表示选出 \(k\) 个点,依次将以每个点为根的内向树边权赋值为 \(0\),需要求出最后树的边权之和的最小值。
当 \(k=1\) 的时候,我们求出 \(w_x\) 表示以 \(x\) 为根的内向树边权和,总和减去 \(\max\{w\}\) 即为答案,\(w\) 可以用换根 DP 求得。
考虑 \(k > 1\) 的情况,有一个关键结论是询问 \(k + 1\) 的答案一定是 \(k\) 的答案基础上,加上一个点。
注意当 \(k = 1\) 的时候结论不成立!只有 \(k > 1\) 时结论成立。我开始猜了这个结论敲暴力验证了一下 \(k = 1 \to k =2\) 发现不满足条件就排除了,结果裂开。以后做这种结论题还要考虑到 corner case。
我们先考虑 \(k = 2\) 怎么做,不难直接推出如果选择两个点 \(x,y\),最大删除的边的和为 \(\dfrac{w_x + w_y + dis(x,y)}{2}\),其中 \(dij(x,y)\) 表示 \(x,y\) 之间路径的双向边权和。这个式子是直径的格式,直接二次扫描换根可以求得。
然后在这条直径的基础上增加点,就相当于将直径缩成一个点,并以之为根,每次增加一条根到叶子的路径。这是个经典问题,直接长链剖分后排序选择即可。
那么这个关键结论怎么证明呢,因为 \(k = 2\) 时选择的是直径,所以在此基础上新增一个点,不会修改原直径,因为不可能有比直径更长的路径。\(k = 1\) 就纯属只存在一个点的特例,所以不满足结论。
时间复杂度 \(\mathcal{O}(N\log N)\),瓶颈在于排序,基数排序可以优化至线性。
#define N 200005
int n, m, h[N], tot = 1; LL w[N], sum, ed[N], d[N], f[N], v[N];
struct edge{int to, nxt, val;}e[N << 1];
void add(int x,int y,int z){e[++tot].nxt = h[x], h[x] = tot, e[tot].to = y, e[tot].val = z;}
void dfs(int x,int fa){for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa)dfs(e[i].to, x), w[x] += w[e[i].to] + e[i].val;}
void calc(int x,int fa){for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa)w[e[i].to] = w[x] - e[i].val + e[i ^ 1].val, calc(e[i].to, x);}
void Dfs(int x,int fa){f[x] = fa; for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa)d[e[i].to] = d[x] + e[i].val + e[i ^ 1].val, Dfs(e[i].to, x);}
vector<LL>c;
LL solve(int x,int fa){
LL cur = 0;
for(int i = h[x]; i; i = e[i].nxt)if(e[i].to != fa){
LL w = e[i ^ 1].val + solve(e[i].to, x);
if(!cur)cur = w;
else c.pb(min(cur, w)), cmx(cur, w);
}return cur;
}
int main() {
read(n);
rp(i, n - 1){
int x, y, l, r;
read(x, y, l, r), sum += l + r;
add(x, y, r), add(y, x, l);
}
dfs(1, 0), calc(1, 0);
rp(i, n)cmx(ed[1], w[i]);
Dfs(1, 0); int A = 1;
rp(i, n)if(d[i] + w[i] > d[A] + w[A])A = i;
d[A] = 1; Dfs(A, 0); int B = 1;
rp(i, n)if(d[i] + w[i] > d[B] + w[B])B = i;
ed[2] = (d[B] + w[A] + w[B]) / 2; int x = B;
while(x)v[x] = 1, x = f[x];
x = B; while(x){
for(int i = h[x]; i; i = e[i].nxt)if(!v[e[i].to])
c.pb(solve(e[i].to, x) + e[i ^ 1].val);
x = f[x];
}
sort(c.begin(), c.end()), reverse(c.begin(), c.end());
int t = 2;
go(x, c)ed[t + 1] = ed[t] + x, t++;
while(t < n)ed[t + 1] = ed[t], t++;
read(m); while(m--){int x; read(x); printf("%lld\n", sum - ed[x]);}
return 0;
}
「JOISC 2019 Day3」开关游戏
给定两个 \(0/1\) 串 \(s,t\),每次可以选择一个区间进行赋值/取反操作,问最少次数将 \(s\) 变成 \(t\)。
关键结论:先取反再进行赋值操作。如果先赋值再取反,赋值的一段还是相同的,一定可以交换顺序变成先取反再赋值。
这样我们就可以直接 DP 了,\(f_{i,0/1/2,0/1}\) 表示以 \(i\) 结尾,是否有以 \(i\) 结尾的赋值操作,是否有以 \(i\) 结尾的取反操作,直接转移就是线性的了。
#define N 1000005
int n, f[N][3][2]; char s[N], t[N];
int main() {
read(n), scanf("%s%s", s + 1, t + 1);
memset(f, 0x3f, sizeof(f)), f[0][2][0] = 0;
rp(i, n){
rep(x, 0, 1){
int y = x != (t[i] - '0');
rep(l, 0, 2)rep(r, 0, 1)cmn(f[i][x][y], f[i - 1][l][r] + (l != x) + (y && !r));
}
int y = s[i] != t[i];
rep(l, 0, 2)rep(r, 0, 1)cmn(f[i][2][y], f[i - 1][l][r] + (y && !r));
}
int ans = inf;
rep(x, 0, 2)rep(y, 0, 1)cmn(ans, f[n][x][y]);
cout << ans << endl;
return 0;
}
「JOISC 2019 Day3」穿越时空 Bitaro
有 \(N\) 个点,相邻两点之间路径开放的时间是 \([l_i,r_i]\),经过一条路需要 \(1\) 的时间。每次操作可以让当前时间 \(-1\),多次询问第 \(x\) 秒从 \(s\) 出发,第 \(y\) 秒到 \(t\) 结束最少进行的操作次数,单点修改。
令当前时间为 \(t\),对于一条路径,就需要花费 \(\max\{0, t - r_i\}\),然后将 \(t\) 对 \(l\) 取 \(\max\),对 \(r\) 取 \(\min\)。
我们发现对于两个相邻的路径可以合并成一个新的路径,即对 \([l,r]\) 取交,如果有交集,那么可以直接看成一条新的路径。如果没有交,那么无论初始时间是什么,最后离开路径的时间是相同的,分开讨论即可。具有结合律,这样就可以用线段树维护。
#define N 300005
int n, m, ed[N], t; Pr u[N];
struct node{
int l, r, p, w;
void ins(int ll,int rr){
l = ll, r = rr, w = 0;
if(ll == rr)p = ll;
}
};
node operator+(node x, node y){
if(x.l < x.r){
if(y.l == y.r){
if(y.p >= x.r)return node{y.l, y.r, x.r, y.w};
if(y.p <= x.l)return node{y.l, y.r, x.l, y.w + x.l - y.p};
return node{y.l, y.r, y.p, y.w};
}
if(y.l >= x.r)return node{y.l, y.l, x.r, 0};
if(y.r <= x.l)return node{y.r, y.r, x.l, x.l - y.r};
return node{max(x.l, y.l), min(x.r, y.r), 0, 0};
}
if(y.l == y.r){
return node{y.l, y.l, x.p, x.w + y.w + max(0LL, x.l - y.p)};
}
if(y.l >= x.r)return node{y.l, y.l, x.p, x.w};
if(y.r <= x.l)return node{y.r, y.r, x.p, x.w + x.l - y.r};
return node{x.l, x.r, x.p, x.w};
}
struct Node{ int l, r; node val;}a[N << 2];
#define L a[x].l
#define R a[x].r
#define ls (x << 1)
#define rs (ls | 1)
#define S a[x].val
void build(int x,int l,int r){
L = l, R = r;
if(l == r)S.ins(u[l].fi - l, u[l].se - l - 1);
else{
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
S = a[ls].val + a[rs].val;
}
}
void ins(int x,int pos,Pr w){
if(L == R)S.ins(w.fi, w.se);
else{
int mid = (L + R) >> 1;
if(mid >= pos)ins(ls, pos, w);
else ins(rs, pos, w);
S = a[ls].val + a[rs].val;
}
}
node ask(int x,int l,int r){
if(L >= l && R <= r)return S;
int mid = (L + R) >> 1;
if(mid >= l)return mid < r ? ask(ls, l, r) + ask(rs, l, r) : ask(ls, l, r);
return ask(rs, l, r);
}
struct query{int op, x, y, z, k;}q[N];
signed main() {
read(n, m);
rp(i, n - 1)read(u[i].fi, u[i].se);
if(n > 1)build(1, 1, n - 1);
rp(i, m){
int op, x, y, z, k;
read(op, x, y, z);
if(1 == op){
ins(1, x, mp(y - x, z - x - 1));
q[i] = {-1, n - x, y, z, 0};
}
else{
read(k);
if(x == z)ed[++t] = max(0LL, y - k);
else if(x < z){
y -= x, k -= z; node w = ask(1, x, z - 1);
if(w.l == w.r)ed[++t] = w.w + max(0LL, y - w.p) + max(0LL, w.l - k);
else ed[++t] = max(0LL, y - w.r) + max(0LL, max(w.l, min(y, w.r)) - k);
}
else{
q[i] = {++t, n - x + 1, y, n - z + 1, k};
}
}
}
reverse(u + 1, u + n);
if(n > 1)build(1, 1, n - 1);
rp(i, m){
if(-1 == q[i].op)ins(1, q[i].x, mp(q[i].y - q[i].x, q[i].z - q[i].x - 1));
else if(q[i].op){
int x = q[i].x, y = q[i].y, z = q[i].z, k = q[i].k;
y -= x, k -= z; node w = ask(1, x, z - 1);
if(w.l == w.r)ed[q[i].op] = w.w + max(0LL, y - w.p) + max(0LL, w.l - k);
else ed[q[i].op] = max(0LL, y - w.r) + max(0LL, max(w.l, min(y, w.r)) - k);
}
}
rp(i, t)printf("%lld\n", ed[i]);
return 0;
}
「JOISC 2019 Day4」蛋糕拼接 3
给定 \(N\) 个二元组 \((V_i,C_i)\),选出 \(M\) 个排成一个环,收益是 \(\sum V_{P_i} - \sum |C_{P_i} - C_{P_{i - 1}}|\),求最大收益。
显然我们将选出的 \(M\) 个元素按 \(C\) 排序的收益是最大的,收益就是 \(\sum V - 2(C_{\max} - C_{\min})\),我们枚举 \(C\) 最小的 \(l\),和最大的 \(r\),然后从区间 \([l,r]\) 中选择最大的 \(M\) 个 \(V\) 即为答案。
这样我们用可持久化线段树可以 \(\mathcal{O}(\log)\) 的时间内求出 \(f(l,r)\) 表示固定最小的 \(l\) 和最大的 \(r\) 后的答案,再猜一个结论, \(g_r\) 表示能使 \(f_{l,r}\) 取得最大值的 \(l\),那么 \(g_r\) 不减,即答案具有决策单调性。写个 1d1d 分治即可。
#define N 200005
int rt[N], idx, n, m, b[N], t; LL ans = Inf_;
struct Node{int l, r, sz; LL sum;}a[N << 5];
void ins(int &x,int y,int l,int r,int pos,int val){
x = ++idx, a[x] = a[y], a[x].sz++, a[x].sum += val;
if(l == r)return;
int mid = (l + r) >> 1;
if(mid >= pos)ins(a[x].l, a[y].l, l, mid, pos, val);
else ins(a[x].r, a[y].r, mid + 1, r, pos, val);
}
LL ask(int x,int y,int l,int r,int k){
if(l == r)return k * (LL)b[l];
int mid = (l + r) >> 1, rs = a[a[x].r].sz - a[a[y].r].sz;
if(rs >= k)return ask(a[x].r, a[y].r, mid + 1, r, k);
return ask(a[x].l, a[y].l, l, mid, k - rs) + a[a[x].r].sum - a[a[y].r].sum;
}
struct node{
int w, c;
bool operator<(const node o)const{return c < o.c;}
}u[N];
LL calc(int l,int r){
return ask(rt[r], rt[l - 1], 1, t, m) - 2 * (u[r].c - u[l].c);
}
void solve(int l,int r,int L,int R){
if(l > r)return;
int x = (l + r) >> 1, rr = min(R, x - m + 1), p = 0; LL mx = Inf_;
rep(i, L, rr){
LL w = calc(i, x);
if(w >= mx)mx = w, p = i;
}
cmx(ans, mx), solve(l, x - 1, L, p), solve(x + 1, r, p, R);
}
int main() {
read(n, m);
rp(i, n)read(u[i].w, u[i].c), b[i] = u[i].w;
sort(b + 1, b + n + 1), sort(u + 1, u + n + 1);
t = unique(b + 1, b + n + 1) - b - 1;
rp(i, n)ins(rt[i], rt[i - 1], 1, t, lower_bound(b + 1, b + t + 1, u[i].w) - b, u[i].w);
solve(m, n, 1, n);
printf("%lld\n", ans);
return 0;
}
「JOISC 2019 Day4」合并
给定一棵树,每个点有一种颜色,如果能将颜色分为两组,使得两组内的点各构成一个连通块,则不合法。问最少合并多少种颜色使得树合法。
不合法的充要条件是存在一条边,不存在颜色在边的两侧都有。
所以对于每种颜色,将其构成的虚树缩成一个点。缩完后还是一棵树,这时候所有的点两两不同,那么答案就是 \(\left\lfloor\dfrac{Leaf+1}{2}\right\rfloor\)。
#define N 500005
int n, m, f[N][20], t, d[N], s[N];
vector<int>e[N], c[N];
void dfs(int x,int fa){
d[x] = 1 + d[f[x][0] = fa];
rp(i, t)f[x][i] = f[f[x][i - 1]][i - 1];
go(y, e[x])if(y != fa)dfs(y, x);
}
int lca(int x,int y){
if(d[x] < d[y])swap(x, y);
pre(i, t, 0)if(d[f[x][i]] >= d[y])x = f[x][i];
if(x == y)return x;
pre(i, t, 0)if(f[x][i] != f[y][i])x = f[x][i], y = f[y][i];
return f[x][0];
}
void link(int x,int y){s[x]++, s[y]++, s[lca(x, y)] -= 2;}
int fa[N], w[N];
void calc(int x,int pa){
go(y, e[x])if(y != pa)calc(y, x), s[x] += s[y];
if(s[x])fa[x] = pa; else fa[x] = x;
}
int get(int x){return fa[x] == x ? x : fa[x] = get(fa[x]);}
int main() {
read(n, m), t = log2(n);
rp(i, n - 1){
int x, y; read(x, y);
e[x].pb(y), e[y].pb(x);
}
rp(i, n){
int x; read(x);
c[x].pb(i);
}
dfs(1, 0);
rp(i, m){
int k = si(c[i]) - 1;
rp(j, k)link(c[i][0], c[i][j]);
}
calc(1, 0);
rp(x, n)go(y, e[x]){
int p = get(x), q = get(y);
if(p != q)w[p] ++;
}
int sum = 0;
rp(i, n)if(fa[i] == i)sum += w[i] == 1;
printf("%d\n", (sum + 1) / 2);
return 0;
}
「JOISC 2019 Day4」矿物
交互题,给定 \(N\) 种颜色,每种颜色恰好 \(2\) 个球,每次可以向集合中插入/删除一个球,然后得到集合中有多少种颜色。你需要在 \(10^6\) 次操作内将球两两配对 \(N\le 4.3\times 10^4\)。
首先不难想到生日悖论,每次随机向集合中加入一个球,当集合中出现相同的球时就配对,然后清空集合。这样做期望次数是 \(\mathcal{O}(N\sqrt N)\),实际得分 \(6\) 分。
这个数据范围显然是留给 \(\log\) 做法的,我们优先考虑分治。现在我们 solve(S)
表示将集合 \(S\) 中的球配对。假设一共有 \(n\) 对,取 \(m = n / 2\),将集合中的球依次加入直到有 \(m\) 对同色球,然后再扫一遍将集合中没有配对的球删去就可以得到 \(\mathcal{O}(N\log N)\) 的算法。但是常数非常大,只能得到 \(40\) 分。
想办法优化常数,如果我们在调用 solve(S)
的时候,已经将每对球中恰好一个球加入集合中,那么我们扫一遍可以同时分出两组 \(m\) 对球,并且每对球都是恰好一个在集合中。这样做每次 solve
的操作次数都是严格小于 \(|S|\) 次,但由于毒瘤出题人,仍只有 \(40\) 分,但是操作次数已经由 \(2\times 10^6\) 优化到 \(1.3\times 10^6\)。
我们发现操作多的原因在于扫的时候,如果一个球不能配对,就需要再操作一次把它放回集合。我们观察一下发现,我们不需要每对球恰好一个在集合中,我们只需要将 \(S\) 能分成的两组 \(A,B\) 使得每组中不存在相同的颜色,而这可以通过最开始扫一遍将每个球染色即可。这样每次 solve
的操作次数都是严格小于 \(\frac{3}{4}|S|\),可以得到 \(85\) 分。
由于出题人丧心病狂的卡常,最后一个点多了大概一万次操作我们注意到每次 solve
有 \(\frac{3}{4}\) 的常数,那么我们每次在中点分治并不是最优的,将分治点偏移一点,调参就过了。
#define S 86005
mt19937 rd(time(0));
int v[S], lst = 0, idx, id[S];
int Query(int);
void Answer(int,int);
int ask(int x){v[x] ^= 1; return Query(x);}
void solve(vector<int>c){
int n = si(c);
if(n == 2){Answer(c[0], c[1]); return ;}
vector<int>p, q, l, r;
go(x, c)if(id[x])p.pb(x); else q.pb(x);
int m = max(1, (int)(n * 0.185)), s = 0;
rep(i, 0, m - 1)lst = ask(q[i]), r.pb(q[i]);
rep(i, m, si(q) - 1)l.pb(q[i]);
int op = !v[q[0]];
go(x, p){
if(s == m)l.pb(x);
else{
int w = ask(x);
if((w != lst) ^ op)l.pb(x);
else r.pb(x), s++;
lst = w;
}
}
solve(l), solve(r);
}
void Solve(int n){
n <<= 1;
vector<int>a;
rp(i, n)a.pb(i);
shuffle(a.begin(), a.end(), rd);
rp(i, n){
int x = a[i - 1];
int w = ask(x);
if(w == lst)id[x] = 1;
lst = w;
}
solve(a);
}
标签:总结,rp,int,mid,JOISC,2019,solve,read,return 来源: https://www.cnblogs.com/7KByte/p/16324655.html