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)\)。
代码
#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\),支持:
- 选定一条树边 \((u,v)\),令 \(S_u\leftarrow S_u\cup S_v\ ,\ S_v\leftarrow S_u\cup S_v\)。即同时赋值为并集。保证每条树边都会被恰好操作\(1\)次。
- 询问 \(u,v\),回答是否有 \(v\in S_u\)。
- 询问 \(v\),回答 \(\sum_u[v\in S_u]\) 即有多少个集合包含 \(v\)。
题解
首先离线把树建出来,这样相当于每条边有一个权值。
考虑点分治。首先处理 \(2\) 操作。可以发现 \(S_u\) 中要有 \(v\) 当且仅当 \(v\) 到 \(u\) 路径权值单调递增且询问时间大于路径最大权值。这些都可以直接 dfs 解决。
考虑处理 \(3\) 操作。对于每个点分中心我们处理的是该点在其点分子树外的贡献,即对于其他子树询问有多少能够到达,要求同样是路径单调增且询问时间大于路径最大权值。
先处理出上升路径与下降路径,把所有子树按与分治中心连边的权值从大到小排序,显然只有连边权值小的子树才有可能存在路径到连边权值大的子树。
由于下降路径最大值一定大于上升路径,所以只需要将下降路径的权值作为键值。这样等价于一个单点插入,询问 \(\geq\) 某个键值的点数,直接树状数组即可。
分治中心位置的询问要额外判断。
复杂度 \(O(n\log^2 n)\)。
代码
#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)\)。
代码
#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)\)。
代码
#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;
}

浙公网安备 33010602011771号