Baltic Olympiad in Informatics 2021

books servers watchmen prison swaps xanadu
$\color{green}{\texttt{+8}}$ $\color{green}{\texttt{+2}}$ $\color{green}{\texttt{-}}$ $\color{green}{\texttt{+1}}$ $\color{green}{\texttt{-}}$ $\color{green}{\texttt{+1}}$

链接 题面链接

A. books

题意

交互题。给定 $n,k,A$。有一个长度为 $n$ 的递增序列 $a_i$,每次可以询问位置 $i$ 的权值,要求返回一个大小为 $m$ 的集合 $S$,满足 $\sum_{i\in S} a_i\in[A,2A]$。

$n\leq 25000,k\in[3,10]$。询问次数 $40$ 次。

题解

首先考虑如果所有元素都 $<A$ 的情况。可以发现 $[1,k]$ 一定是最小的和,$(n-k+1,n]$ 一定是最大的和。如果最小的和 $>2A$ 或者最大的和 $<A$ 那么直接无解。
否则可以发现相邻区间区间 $[i,i+k)$ 与 $(i,i+k]$ 之差 $<A$,所以连续区间中一定有解。
二分位置 $p$ 查询 $[p,p+k)$ 的和,即可做到 $k\log n$ 次查询。

考虑有元素 $\geq A$ 的情况。其实这个很好处理:二分找到最小的 $x$ 使得 $x\geq A$,那么贪心地选一定取 $[1,k)$ 最优。如果 $[1,k)\cup{x}$ 不合法,那么任何有元素 $\geq A$ 的集合都不合法。

考虑优化询问复杂度。注意到其实我们只要构造一个子序列,满足这个子序列最小子区间 $\leq 2A$,最大子区间 $\geq A$,相邻区间差 $<A$。

考虑先排除有元素 $\geq A$ 的情况,这样相邻区间差一定 $<A$。假设 $n$ 为剩下序列长度。事实上我们只需要保留 $[1,k]\cup(n-k,n]$ 对应的子序列即可,此时上述三个条件都满足。
因为子序列长度只有 $2k$,直接暴力全部查出即可。

分析交互次数,总共 $O(k+\log n)$。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<bits/stdc++.h>
#include"books.h"
using namespace std;
typedef long long ll;
static const int N=100010;
static ll a[N];
ll qry(int x){if(a[x]) return a[x];else return a[x]=skim(x);}
int get_lower(int l,int r,ll v)
{
int p=r;
while(l<=r){int mid=(l+r)>>1;if(qry(mid)>=v) r=mid-1,p=mid;else l=mid+1;}
return p;
}
static int x[24];
void solve(int n,int k,ll lim,int s0)
{
auto check=[&](int x[]){ll s=0;for(int i=1;i<=k;i++) cerr<<x[i]<<" ",s+=qry(x[i]);cerr<<":"<<s<<endl;if(s>=lim && s<=2*lim) answer(vector<int>(x+1,x+k+1));};
ll s=0;
for(int i=1;i<k;i++) x[i]=i,s+=qry(i);
if(s>2*lim) impossible();
n=get_lower(1,n,lim);
x[k]=n;check(x);
cerr<<n<<endl;
int q=max(n-k,0),m=0;
for(int i=1;i<=k;i++) x[++m]=i;
for(int i=1;i<=k;i++) x[++m]=q+i;
sort(x+1,x+m+1);m=unique(x+1,x+m+1)-x;
for(int i=0;i<m-k;i++) check(x+i);
impossible();
}

B. servers

题意

给定一个 $n$ 个点的树,每个点要维护一个集合 $S_u$,支持:

  1. 选定一条树边 $(u,v)$,令 $S_u\leftarrow S_u\cup S_v\ ,\ S_v\leftarrow S_u\cup S_v$。即同时赋值为并集。保证每条树边都会被恰好操作$1$次。
  2. 询问 $u,v$,回答是否有 $v\in S_u$。
  3. 询问 $v$,回答 $\sum_u[v\in S_u]$ 即有多少个集合包含 $v$。

题解

首先离线把树建出来,这样相当于每条边有一个权值。

考虑点分治。首先处理 $2$ 操作。可以发现 $S_u$ 中要有 $v$ 当且仅当 $v$ 到 $u$ 路径权值单调递增且询问时间大于路径最大权值。这些都可以直接 dfs 解决。

考虑处理 $3$ 操作。对于每个点分中心我们处理的是该点在其点分子树外的贡献,即对于其他子树询问有多少能够到达,要求同样是路径单调增且询问时间大于路径最大权值。
先处理出上升路径与下降路径,把所有子树按与分治中心连边的权值从大到小排序,显然只有连边权值小的子树才有可能存在路径到连边权值大的子树。

由于下降路径最大值一定大于上升路径,所以只需要将下降路径的权值作为键值。这样等价于一个单点插入,询问 $\geq$ 某个键值的点数,直接树状数组即可。

分治中心位置的询问要额外判断。

复杂度 $O(n\log^2 n)$。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#define N 250010
#define fi first
#define se second
using namespace std;
int nxt[N<<1],to[N<<1],head[N],w[N<<1],cnt;
void add(int u,int v,int w0){nxt[++cnt]=head[u];to[cnt]=v;w[cnt]=w0;head[u]=cnt;}
bool cut[N];
namespace find_root{
int siz[N];
void dfs0(int u,int p){siz[u]=1;for(int i=head[u];i;i=nxt[i]) if(!cut[to[i]] && to[i]!=p) dfs0(to[i],u),siz[u]+=siz[to[i]];}
int all,mxr,mx;
void dfs1(int u,int p){int r=all-siz[u];for(int i=head[u];i;i=nxt[i]) if(!cut[to[i]] && to[i]!=p) dfs1(to[i],u),r=max(r,siz[to[i]]);if(r<mx) mx=r,mxr=u;}
int root(int u){dfs0(u,0);mx=all=siz[u],mxr=0;dfs1(u,0);return mxr;}
}
using find_root::root;
int ans[N],n;
namespace solveQ{
vector<pair<int,int>>q[N];
int f[N],g[N],vis[N],T;
void dfs_up(int u,int p)
{
vis[u]=T;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];if(v==p || cut[v]) continue;
if(w[i]<f[u]) f[v]=w[i],g[v]=g[u],dfs_up(v,u);
}
}
void dfs_down(int u,int p,int pre,int fi)
{
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];if(v==p || cut[v]) continue;
if(w[i]>pre) dfs_down(v,u,w[i],fi);
}
for(auto w:q[u]) if(w.se>=pre && vis[w.fi]==T && g[w.fi]<=fi) ans[w.se]=true;
}
void solve(int u)
{
++T;
f[u]=1e8;g[u]=0;vis[u]=T;
for(int i=head[u];i;i=nxt[i]){int v=to[i];if(!cut[v]) f[v]=g[v]=w[i],dfs_up(v,u);}
for(int i=head[u];i;i=nxt[i]){int v=to[i];if(!cut[v]) dfs_down(v,u,w[i],w[i]);}
for(auto w:q[u]) if(vis[w.fi]==T && w.se>=g[w.fi]) ans[w.se]=true;
}
}
namespace solveC{
vector<int>q[N];
int tt[N],f[N];
int a[N];
void add(int x,int v){for(;x<=N-10;x+=x&-x) a[x]+=v;}
int qry(int x){int v=0;for(;x;x-=x&-x) v+=a[x];return v;}
void dfs_down(int u,int p,int pre,int f,int val)
{
add(pre,val);
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];if(v==p || cut[v]) continue;
if(w[i]>pre) dfs_down(v,u,w[i],f,val);
}
}
void dfs_up(int u,int p,int pre,int f)
{
for(int v:q[u]) if(v>f) ans[v]+=qry(v)+1;//+1 is for the root
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];if(v==p || cut[v]) continue;
if(w[i]<pre) dfs_up(v,u,w[i],f);
}
}
void solve(int u)
{
vector<int>son;
for(int i=head[u];i;i=nxt[i]){int v=to[i];if(!cut[v]) son.push_back(v),f[v]=w[i];}
sort(son.begin(),son.end(),[&](int x,int y){return f[x]>f[y];});
for(int v:son)
{
dfs_up(v,u,f[v],f[v]);
dfs_down(v,u,f[v],f[v],1);
}
for(int v:q[u]) ans[v]+=qry(v)+1;
for(int v:son) dfs_down(v,u,f[v],f[v],-1);
}
}
void solve(int u)
{
u=root(u);cut[u]=true;
solveQ::solve(u);
solveC::solve(u);
for(int i=head[u];i;i=nxt[i]){int v=to[i];if(!cut[v]) solve(v);}
}
int is_qry[N];
int main()
{
int k;scanf("%d%d",&n,&k);
int m=0;
for(int i=1;i<n+k;i++)
{
char op[2];int x,y;scanf("%s%d",op,&x);
if(op[0]=='S') scanf("%d",&y),add(x,y,i),add(y,x,i);
else if(op[0]=='Q') scanf("%d",&y),++m,solveQ::q[x].emplace_back(y,i),is_qry[i]=1;
else ++m,solveC::q[x].push_back(i),is_qry[i]=2;
}
solve(1);
for(int i=1;i<n+k;i++) if(is_qry[i]==1) puts(ans[i]?"yes":"no");
else if(is_qry[i]==2) printf("%d\n",ans[i]);
return 0;
}

D. prison

首先显然有一个 $O(n\log^2 n)$ 的垃圾做法:

令 $l_i$ 表示如果 $i$ 要暴动,$[ l_i,i]$ 范围内不能有垫子。转化成平面上有若干线段,选 $k$ 个点使尽可能多的线段包含这些点。
显然这可以建出最小割模型故函数是凸的,用 wqs 二分 + 线段树即可做到 $O(n\log^2 n)$。

考虑一个结论:$\forall i< j\ ,\ l_j\geq i\ \vee\ l_j\leq l_i$。这个很好证明,因为如果一个点能经过 $i$ 传到 $j$ 那么 $i$ 一定也动了。 这样任意两个线段要么相离要么包含。
令每个点的父亲为包含它的,题意转化成:给定一个森林,将 $d$ 个叶子到根路径染黑,问最多能染黑几个。

考虑一个贪心做法:每次选择一个到根白点最多的叶子染黑。证明可以考虑每次取一个点向上第一个没有被染黑的节点 $u$,那么 $u$ 为根子树内最深的点是 $u$ 中最优的点。

如果删除染黑的点,那么一次操作会将一棵树分裂为若干森林。不妨用堆维护森林的根对应子树深度最大值,容易发现一个点只会被加入与删除一次,故复杂度 $O(n\log n)$,可以通过。

而上述过程中所有值都 $\leq n$,所以用桶代替堆同时用一个指针枚举当前最大值可以做到 $O(n)$。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#define N 2000010
using namespace std;
int t[N],a[N];
int id[N],b[N],tp;
vector<int>g[N];int fa[N];
struct node{
int u,v;
node(int U=0,int V=0):u(U),v(V){}
};
bool operator <(const node a,const node b){return a.v==b.v?a.u<b.u:a.v<b.v;}
node mx[N];
bool vis[N];
void dfs(int u)
{
mx[u]=node(u,0);
for(int v:g[u]) dfs(v),mx[u]=max(mx[u],mx[v]);
mx[u].v++;
}
priority_queue<node>q;
int main()
{
int n,m,T;scanf("%d%d%d",&n,&m,&T);
int res=0;
for(int i=1;i<=n;i++) scanf("%d",&t[i]);
for(int i=1;i<=n;i++)
if(t[i]<=T)
{
a[i]=i;++res;
while(tp && b[tp]>=t[i]-i) tp--;
id[++tp]=i;b[tp]=t[i]-i;
}
else
{
while(tp && b[tp]+i>T) tp--;
a[i]=id[tp];
}
tp=0;
for(int i=n;i;i--) if(i!=a[i] && a[i]!=0)
{
++res;
while(tp && b[tp]>a[i]) tp--;
g[fa[i]=id[tp]].push_back(i);
id[++tp]=i,b[tp]=a[i];
}
for(int u:g[0]) dfs(u),q.push(mx[u]);
while(!q.empty() && m)
{
int u=q.top().u;q.pop();--m;
for(;u && !vis[u];u=fa[u])
{
vis[u]=true;
for(int v:g[u]) if(!vis[v]) q.push(mx[v]);
--res;
}
}
printf("%d\n",res);
return 0;
}

F. xanadu

全场真正的签到题。

直接 dp,用 $f_{i,0/1,0/1}$ 表示点 $i$,当前为 $0/1$,按过 $0/1$ 次按钮。

dp 时记录 $w_{0/1,0/1}$ 表示儿子全部为开/关,根是否被翻转。

复杂度 $O(n)$。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define N 200010
#define inf 1000000000
#define ll long long
using namespace std;
vector<int>g[N];
ll f[N][2][2];int a[N];
//f_{i,0/1,0/1} 点 i,当前为 0/1,按过 0/1 次按钮
void dfs(int u,int p)
{
ll h[2][2]={{0,inf},{0,inf}},w[2][2];//所有儿子都是 0/1 且翻转 0/1 次
for(int v:g[u]) if(v!=p)
{
dfs(v,u);
memset(w,0,sizeof(w));
w[0][0]=min(h[0][0]+f[v][0][0],h[0][1]+f[v][0][1]);
w[0][1]=min(h[0][0]+f[v][0][1],h[0][1]+f[v][0][0]);
w[1][0]=min(h[1][0]+f[v][1][0],h[1][1]+f[v][1][1]);
w[1][1]=min(h[1][0]+f[v][1][1],h[1][1]+f[v][1][0]);
memcpy(h,w,sizeof(w));
}
f[u][0][0]=h[0][a[u]];
f[u][0][1]=h[1][!a[u]]+1;
f[u][1][0]=h[0][!a[u]];
f[u][1][1]=h[1][a[u]]+1;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1,u,v;i<n;i++) scanf("%d%d",&u,&v),g[u].push_back(v),g[v].push_back(u);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
dfs(1,0);
ll res=min(f[1][0][0],f[1][0][1]);
if(res>=inf) puts("impossible");
else printf("%lld\n",res);
return 0;
}