Baltic Olympiad in Informatics 2017

A B C D E F
$\color{green}{\texttt{+3}}$ $\color{green}{\texttt{+}}$ $\color{green}{\texttt{+3}}$ $\color{green}{\texttt{+}}$ $\color{red}{\texttt{+5}}$ $\color{green}{\texttt{+}}$

链接 题面链接 中文题面

A. Political Development

首先至少有一个点度数 $\leq k$,删去这个点后,同样至少有一个点度数 $\leq k$。

把与即将被删点相邻的至多 $k+1$ 个点拿出来跑一边最大独立集。复杂度 $O(nk2^k)$。

代码
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
#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<vector>
#define N 100010
using namespace std;
int deg[N];
set<pair<int,int>>s;
set<int>g[N];
int ans,as,f[20];
void solve(vector<int>&v)
{
int n=v.size();
for(int i=0;i<n;i++)
{
f[i]=0;
for(int j=0;j<n;j++)
if(i!=j && g[v[i]].count(v[j])) f[i]|=1<<j;
}
for(int s=1;s<1<<n;s++)
{
bool can=true;
for(int i=0;i<n && can;i++) if(s>>i&1 && (f[i]&s)!=(s^(1<<i))) can=false;
if(can) ans=max(ans,__builtin_popcount(s));
}
}
bool vis[N];
int main()
{
int n,k;scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
{
int x,y;scanf("%d",&x);deg[i]=x;
while(x --> 0) scanf("%d",&y),g[i].insert(y);
}
for(int i=0;i<n;i++) s.emplace(deg[i],i);
while(!s.empty())
{
int u=s.begin()->second;s.erase(s.begin());
vis[u]=true;
vector<int>p;p.push_back(u);
for(int v:g[u]) if(!vis[v]) p.push_back(v);
solve(p);
for(int v:p) if(!vis[v]) s.erase({deg[v],v}),s.emplace(--deg[v],v);
}
printf("%d\n",ans);
return 0;
}

B. Railway

用类似于虚树的想法,把每次要求的点按 dfn 排序,那么每个点贡献的边就是该点到该点和上一个点的 lca 部分。

直接离线后 dfs 一遍即可。

复杂度 $O(n\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
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
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<map>
#include<algorithm>
#define N 100010
using namespace std;
vector<int>g[N];
int dep[N],siz[N],son[N],fa[N];
map<pair<int,int>,int>mp;
void dfs(int u,int p)
{
siz[u]=1;dep[u]=dep[p]+1;fa[u]=p;
for(int v:g[u]) if(v!=p)
{
dfs(v,u);siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
int top[N],dfn[N],idx;
void dfs2(int u,int topp)
{
top[u]=topp;dfn[u]=++idx;
if(son[u]) dfs2(son[u],topp);
for(int v:g[u]) if(v!=fa[u] && v!=son[u]) dfs2(v,v);
}
int lca(int x,int y)
{
for(;top[x]!=top[y];x=fa[top[x]]) if(dep[top[x]]<dep[top[y]]) swap(x,y);
return dep[x]<dep[y]?x:y;
}
int w[N],ans[N],p[N];
void dfs3(int u){for(int v:g[u]) if(v!=fa[u]) dfs3(v),w[u]+=w[v];}
int main()
{
int n,m,k;scanf("%d%d%d",&n,&m,&k);
for(int i=1,u,v;i<n;i++) scanf("%d%d",&u,&v),g[u].push_back(v),g[v].push_back(u),mp[{u,v}]=mp[{v,u}]=i;
dfs(1,0);dfs2(1,1);
for(int i=1;i<=m;i++)
{
int q;scanf("%d",&q);
for(int i=1;i<=q;i++) scanf("%d",&p[i]);
sort(p+1,p+q+1,[&](int x,int y){return dfn[x]<dfn[y];});
w[p[1]]++;int l=p[1];
for(int i=2;i<=q;i++) w[p[i]]++,w[lca(p[i],p[i-1])]--,l=lca(l,p[i]);
w[l]--;
}
dfs3(1);
for(int i=2;i<=n;i++) ans[mp[{fa[i],i}]]=w[i]>=k;
int tt=0;
for(int i=1;i<n;i++) tt+=ans[i];
printf("%d\n",tt);
for(int i=1;i<n;i++) if(ans[i]) printf("%d ",i);
return 0;
}

C. Toll

观察到题目给的其实是一个分层图,且每层至多 $k$ 个点,$k\leq 5$。

直接用 $f_{i,x,y}$ 表示第 $i$ 层的 $x$ 到 $i+1$ 层的 $y$ 的最短距离,这显然可以写成 $\max$ 矩阵的形式。题意等同于问区间矩阵乘积,可以直接分治解决,复杂度 $O(nk^3\log q)$,常数极小。

代码
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
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define N 100010
#define M 5
#define fi first
#define se second
using namespace std;
int n,m,k,q;
struct matr{
int a[M][M];
matr(){memset(a,0x3f,sizeof(a));}
int* operator [](int x){return a[x];}
}g[N],p[N];
matr operator *(matr x,matr y)
{
matr z;
for(int i=0;i<k;i++)
for(int t=0;t<k;t++)
for(int j=0;j<k;j++) z[i][j]=min(z[i][j],x[i][t]+y[t][j]);
return z;
}
struct node{
int l,lx,r,rx,t;
};
int ans[N];
void solve(int l,int r,vector<node> &Q)
{
if(l==r)
{
for(auto x:Q) if(x.l==x.r) ans[x.t]=g[x.l][x.lx][x.rx];
return;
}
if(Q.empty()) return;
int mid=(l+r)>>1,fl=mid,fr=mid+1;
vector<node> ql,qr;
for(auto x:Q) if(x.l<=mid && mid<x.r) fl=min(fl,x.l),fr=max(fr,x.r);
p[mid]=g[mid];for(int i=mid-1;i>=fl;i--) p[i]=g[i]*p[i+1];
p[mid+1]=g[mid+1];for(int i=mid+2;i<=fr;i++) p[i]=p[i-1]*g[i];
for(auto x:Q) if(x.l<=mid && mid<x.r)
ans[x.t]=(p[x.l]*p[x.r])[x.lx][x.rx];
else if(x.r<=mid) ql.push_back(x);else if(x.l>mid) qr.push_back(x);
solve(l,mid,ql);solve(mid+1,r,qr);
}
int main()
{
scanf("%d%d%d%d",&k,&n,&m,&q);
int mx=(n+k-1)/k;
for(int i=1,u,v,w;i<=m;i++) scanf("%d%d%d",&u,&v,&w),g[u/k][u%k][v%k]=w;
vector<node>Q;
for(int i=0,x,y;i<q;i++) scanf("%d%d",&x,&y),Q.push_back({x/k,x%k,y/k-1,y%k,i});
memset(ans,-1,sizeof(ans));
solve(0,mx-1,Q);
for(int i=0;i<q;i++) printf("%d\n",ans[i]>=1e9?-1:ans[i]);
return 0;
}

D. Cat in a tree

一开始想了一个反悔堆贪心,后来发现完全没有必要。

首先反悔堆贪心比较显然:用小根堆记录每个子树里面选的深度,每次合并的时候判断两个堆顶是否不合法,如果不合法贪心删去较浅的那一个,容易证明是正确的。

然而事实上一个堆只会被至多弹依次。如果一个子树中 $a,b$ 被选择了,另一个子树中 $c,d$ 被选择了,要想某个堆弹出两次,需要 $\text{dis}(a,c)< m,\text{dis}(b,d)< m$,然而 $\text{dis(a,b)}+\text{dis}(c,d)<\text{dis}(a,c)+\text{dis}(b,d)$,故 $\text{dis}(a,c)+\text{dis}(b,d)> 2m$。所以一个堆至多被弹一次。

那么只需要记录一个点子树内最浅的位置,然后每次扫子树过程中发现不合法,就将最浅的位置删掉即可。

复杂度 $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
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#define N 200010
using namespace std;
int f[N],ans,m;
vector<int>g[N];
void dfs(int u,int p)
{
f[u]=m;
for(int v:g[u])
{
dfs(v,u);
if(f[u]+f[v]+1<m) --ans,f[u]=max(f[u],f[v]+1);
else f[u]=min(f[u],f[v]+1);
}
if(f[u]>=m) ++ans,f[u]=0;
}
int main()
{
int n;scanf("%d%d",&n,&m);
for(int i=2,u;i<=n;i++) scanf("%d",&u),g[u+1].push_back(i);
dfs(1,0);
printf("%d",ans);
return 0;
}

E. Friends

很妙的题。

考虑 $p+q$ 很小,采用暴力增广,即每次从一个点 $i$ 开始,暴力枚举其所有出边。如果某一时刻集合出边个数加上集合内点数超过 $p+q$ 直接无解。否则枚举每条出边是否塞进集合,总共只有 $2^{p+q}$ 种可能。

算出所有集合后考虑一个性质:如果 $A,B$ 都是合法集合,那么 $A\backslash B$ 和 $B\backslash A$ 中必然有一个是合法的。

考虑反证,显然点数不会不合法。假设分开集合为 $A,B$,公共部分为 $C$。

如果边数不合法,假设 $A$ 向 $B$ 连边为 $a$(粉色边),$A$ 向 $C$ 连边为 $b$(黄色边),$B$ 向 $C$ 连边为 $c$(蓝色边),$A,C,B$ 向外面连边为 $d,e,f$。

那么有 $a+b+d>q,a+c+f>q$,因为 $A,B$ 要合法,有 $a+c+d+e\leq q,a+b+e+f\leq q$,$2a+b+c+d+2e+f\leq 2q<2a+b+c+d+f$,推出 $e<0$,不合法。

所以只需要找出所有初始集合,然后调整即可。

复杂度 $O(n2^{p+q}+n^2p)$。

代码
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
#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<queue>
#define N 5100
using namespace std;
set<int>g[N];
int n,m1,m2;
bool check(const set<int>&s)
{
if(s.size()>m1) return false;
int tt=0;
for(int u:s)
{
if(g[u].size()>m1+m2) return false;
for(int v:g[u]) if(!s.count(v))
{
if(++tt>m2) return false;
}
}
return true;
}
bool vis[N];
set<int>f[N],s,t,o,tmp[N];
int tt;
bool solve()
{
if(s.size()>m1 || s.size()+t.size()+o.size()>m1+m2 || o.size()>m2) return false;
if(t.empty())
{
if(check(s))
{
for(int u:s) vis[u]=true;
f[++tt]=s;
return true;
}
return false;
}
int u=*t.begin();
tmp[u]=t;t.erase(u);
if(!s.empty())
{
o.insert(u);
if(solve()) return true;
o.erase(u);
}
s.insert(u);
for(int v:g[u]) if(!s.count(v) && !t.count(v) && !o.count(v)){t.insert(v);if(s.size()+t.size()>m1+m2) break;}
if(solve()) return true;
s.erase(u);
t=tmp[u];
return false;
}
void adjust(int x,int y)
{
bool hv=true;
for(int v:f[x]) if(f[y].count(v)){hv=false;break;}
if(hv) return;
set<int>z=f[x];
for(int v:f[y]) if(z.count(v)) z.erase(v);
if(check(z)){f[x]=z;return;}
z=f[y];
for(int v:f[x]) if(z.count(v)) z.erase(v);
if(check(z)){f[y]=z;return;}
throw;
}
set<set<int>>ans;
int main()
{
scanf("%d%d%d",&n,&m1,&m2);
for(int i=1;i<=n;i++)
{
int k;scanf("%d",&k);
for(int j=1,x;j<=k;j++) scanf("%d",&x),g[i].insert(++x);
}
for(int i=1;i<=n;i++)
for(int v:g[i]) if(!g[v].count(i)){puts("detention");return 0;}
for(int i=1;i<=n;i++)
if(!vis[i])
{
s=o={},t={i};
if(!solve()){puts("detention");return 0;}
}
puts("home");
for(int i=1;i<=tt;i++)
for(int j=i+1;j<=tt;j++) adjust(i,j);
for(int i=1;i<=tt;i++) if(!f[i].empty()) ans.insert(f[i]);
printf("%d\n",(int)ans.size());
for(auto &s:ans)
{
printf("%d ",(int)s.size());
for(int u:s) printf("%d ",u-1);
puts("");
}
return 0;
}

F. Plus Minus

签到题,可以发现要么行 $+/-$ 交替,要么列 $+/-$ 交替,最后减去重复部分(行列都 $+/-$ 交替)即可。

算出每种情况可以自由选择的个数 $v$,那么贡献就是 $2^v$。

代码
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
#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
#define N 100010
#define mod 1000000007
using namespace std;
int x[N],y[N],op[N];char s[3];
int ksm(int a,int b=mod-2)
{
if(b<0) return 0;
int r=1;
for(;b;b>>=1,a=1ll*a*a%mod) if(b&1) r=1ll*r*a%mod;
return r;
}
map<int,int>mp;
int main()
{
int n,m,k;scanf("%d%d%d",&n,&m,&k);
if(k==0){printf("%d",(ksm(2,n)+ksm(2,m)-2)%mod);return 0;}
for(int i=1;i<=k;i++) scanf("%s%d%d",s,&x[i],&y[i]),op[i]=s[0]=='+';
int tt=n;
for(int i=1;i<=k;i++)
if(!mp.count(x[i])) mp[x[i]]=(y[i]^op[i])&1,tt--;
else if(mp[x[i]]!=((y[i]^op[i])&1)){tt=-1;break;}
int ans=ksm(2,tt);
tt=m;
mp.clear();
for(int i=1;i<=k;i++)
if(!mp.count(y[i])) mp[y[i]]=(x[i]^op[i])&1,tt--;
else if(mp[y[i]]!=((x[i]^op[i])&1)){tt=-1;break;}
ans=(ans+ksm(2,tt))%mod;
int al=(x[1]+y[1]+op[1])&1,c=1;
for(int i=2;i<=k;i++) if(al!=((x[i]+y[i]+op[i])&1)){c=0;break;}
ans=(ans-c+mod)%mod;
printf("%d\n",ans);
return 0;
}