Baltic Olympiad in Informatics 2021

books servers watchmen prison swaps xanadu
+8 +2 - +1 - +1

链接 题面链接

A. books

题意

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

n25000,k[3,10]。询问次数 40 次。

题解

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

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

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

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

分析交互次数,总共 O(k+logn)

代码
#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 个点的树,每个点要维护一个集合 Su,支持:

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

题解

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

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

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

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

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

复杂度 O(nlog2n)

代码
#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(nlog2n) 的垃圾做法:

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

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

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

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

而上述过程中所有值都 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,用 fi,0/1,0/1 表示点 i,当前为 0/1,按过 0/1 次按钮。

dp 时记录 w0/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;
}
posted @   Flying2018  阅读(73)  评论(0编辑  收藏  举报
编辑推荐:
· 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编程运行原理
点击右上角即可分享
微信分享提示