Baltic Olympiad in Informatics 2021
链接 题面链接
A. books
题意
交互题。给定 。有一个长度为 的递增序列 ,每次可以询问位置 的权值,要求返回一个大小为 的集合 ,满足 。
。询问次数 次。
题解
首先考虑如果所有元素都 的情况。可以发现 一定是最小的和, 一定是最大的和。如果最小的和 或者最大的和 那么直接无解。
否则可以发现相邻区间区间 与 之差 ,所以连续区间中一定有解。
二分位置 查询 的和,即可做到 次查询。
考虑有元素 的情况。其实这个很好处理:二分找到最小的 使得 ,那么贪心地选一定取 最优。如果 不合法,那么任何有元素 的集合都不合法。
考虑优化询问复杂度。注意到其实我们只要构造一个子序列,满足这个子序列最小子区间 ,最大子区间 ,相邻区间差 。
考虑先排除有元素 的情况,这样相邻区间差一定 。假设 为剩下序列长度。事实上我们只需要保留 对应的子序列即可,此时上述三个条件都满足。
因为子序列长度只有 ,直接暴力全部查出即可。
分析交互次数,总共 。
代码
#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
题意
给定一个 个点的树,每个点要维护一个集合 ,支持:
- 选定一条树边 ,令 。即同时赋值为并集。保证每条树边都会被恰好操作次。
- 询问 ,回答是否有 。
- 询问 ,回答 即有多少个集合包含 。
题解
首先离线把树建出来,这样相当于每条边有一个权值。
考虑点分治。首先处理 操作。可以发现 中要有 当且仅当 到 路径权值单调递增且询问时间大于路径最大权值。这些都可以直接 dfs 解决。
考虑处理 操作。对于每个点分中心我们处理的是该点在其点分子树外的贡献,即对于其他子树询问有多少能够到达,要求同样是路径单调增且询问时间大于路径最大权值。
先处理出上升路径与下降路径,把所有子树按与分治中心连边的权值从大到小排序,显然只有连边权值小的子树才有可能存在路径到连边权值大的子树。
由于下降路径最大值一定大于上升路径,所以只需要将下降路径的权值作为键值。这样等价于一个单点插入,询问 某个键值的点数,直接树状数组即可。
分治中心位置的询问要额外判断。
复杂度 。
代码
#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
首先显然有一个 的垃圾做法:
令 表示如果 要暴动, 范围内不能有垫子。转化成平面上有若干线段,选 个点使尽可能多的线段包含这些点。
显然这可以建出最小割模型故函数是凸的,用 wqs 二分 + 线段树即可做到 。
考虑一个结论:。这个很好证明,因为如果一个点能经过 传到 那么 一定也动了。 这样任意两个线段要么相离要么包含。
令每个点的父亲为包含它的,题意转化成:给定一个森林,将 个叶子到根路径染黑,问最多能染黑几个。
考虑一个贪心做法:每次选择一个到根白点最多的叶子染黑。证明可以考虑每次取一个点向上第一个没有被染黑的节点 ,那么 为根子树内最深的点是 中最优的点。
如果删除染黑的点,那么一次操作会将一棵树分裂为若干森林。不妨用堆维护森林的根对应子树深度最大值,容易发现一个点只会被加入与删除一次,故复杂度 ,可以通过。
而上述过程中所有值都 ,所以用桶代替堆同时用一个指针枚举当前最大值可以做到 。
代码
#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,用 表示点 ,当前为 ,按过 次按钮。
dp 时记录 表示儿子全部为开/关,根是否被翻转。
复杂度 。
代码
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理