Baltic Olympiad in Informatics 2018
| polygon | dna | worm | alternating | genetics | paths |
|---|---|---|---|---|---|
| \(\color{green}{\texttt{+}}\) | \(\color{green}{\texttt{+}}\) | \(\color{red}{\texttt{+}}\) | \(\color{red}{\texttt{-}}\) | \(\color{green}{\texttt{+}}\) | \(\color{green}{\texttt{+}}\) |
链接 题面链接 中文题面
A. Love Polygon
注意到一个点至少有一条出边,而总边数是 \(n\),所以这是一棵基环内向树。
除去原本的二元环,实际上答案就是 \(n-匹配数\)。
找出每一个块,非环部分先贪心匹配。如果环上有点已经被匹配了就从该点开始,否则任取一个点开始贪心匹配,容易证明是正确的。
复杂度 \(O(n)\)
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
#include<vector>
#define N 100010
using namespace std;
map<string,int>mp;
int fa[N];
bool vis[N];int cir[N],T;
vector<int>g[N];
int ans;bool col[N];
void dfs(int u){vis[u]=true;for(int v:g[u]) if(cir[v]!=T){dfs(v);if(!col[u] && !col[v]) col[u]=col[v]=true,++ans;}}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;cin>>n;if(n&1){puts("-1");return 0;}
for(int i=1,tt=0;i<=n;i++)
{
string s1,s2;cin>>s1>>s2;
if(!mp.count(s1)) mp[s1]=++tt;
if(!mp.count(s2)) mp[s2]=++tt;
int u=mp[s1],v=mp[s2];
if(fa[u]){puts("-1");return 0;}
fa[u]=v;g[v].push_back(u);
}
for(int i=1;i<=n;i++) if(!fa[i]){puts("-1");return 0;}
for(int i=1;i<=n;i++) if(!vis[i])
{
int u=i;
++T;for(;cir[u]!=T;u=fa[u]) cir[u]=T;
vector<int>c;
++T;for(;cir[u]!=T;u=fa[u]) cir[u]=T,c.push_back(u);
if(c.size()==2){col[c[0]]=col[c[1]]=true,ans+=2;}
for(int v:c) dfs(v);
if(c.size()<=2) continue;
int w=u;
for(int v:c) if(col[v]){w=v;continue;}
for(int v=fa[w];v!=w;v=fa[v]) if(!col[v] && !col[fa[v]]) col[v]=col[fa[v]]=true,++ans;
}
cout<<n-ans;
return 0;
}
B. Martian DNA
签到题,直接双指针扫过去即可。
复杂度 \(O(n)\)。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 200010
using namespace std;
int res,a[N],d[N],c[N];
void add(int x){if((++c[x])==d[x]) ++res;}
void del(int x){if((c[x]--)==d[x]) --res;}
int main()
{
int n,m,k;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1,x,y;i<=k;i++) scanf("%d%d",&x,&y),d[x]=y;
int ans=n+1;res=m-k;
for(int i=1,j=1;i<=n;i++)
{
while(j<=n && res!=m) add(a[j++]);
if(res!=m) break;
ans=min(ans,j-i);
del(a[i]);
}
if(ans>n) puts("impossible");
else printf("%d\n",ans);
return 0;
}
C. Worm Worries
这道题本质上有三个子任务。
子任务 1 (subtask 1,2)
子任务 1 约束:\(M = K = 1, N = 10^6, Q = 35\)
显然有一个查询次数 \(2\log_2 N\) 的做法:每次查询中点与其右边的点,舍去较小权值对应的一半序列。容易证明,较大权值的部分一定存在可行的峰值。
毛估估一下要 \(40\) 次,恰好过不去。
考虑优化:上述做法每次要用 2 个询问,有点浪费。
事实上如果询问点在 \(x,y\),有 \(a_x<a_y\),那么 \([1,x]\) 部分同样可以舍掉。
事实上取 \(x\) 为黄金分割点(大约为 \(0.38\)),每次将 \(y\) 取另一黄金分割点,每次可以舍去 \(\phi\) 的部分,询问次数为 \(\log_{\phi}n\),算一下大概 \(30\) 次,可以通过。
子任务 2 (subtask 3,4)
子任务 2 约束:\(K = 1, N=M=1000,Q=3500\)
考虑每次取较长边的一半(不妨假设为横坐标),分成两段,找到其中的最大值,扩展最大值周围的格子。
容易证明,当前极值在右边,如果最终极值在左边,一定会有当前极值引出的一条路径穿过中间的查询点,那么一定会被更新最大值。所以如果已知的最大值(包括之前询问得到的)在右边,那么递归处理右边的格子,否则递归左边格子一定合法。
毛估估一下可以通过。
子任务 3 (subtask 5,6)
subtask 5 可以用类似子任务 2 的方法处理。
子任务 3 约束:\(N = M = K = 500, Q = 1.5\times 10^5\)
考虑一种很自然的想法:每次随机 \(\frac Q2\) 个点,取其中最大的点,贪心向其四周查找最大值并转移。
分析一下正确概率:
不妨考虑最坏情况,一次转移认为要 \(6\) 次查询,那么要能在 \(\frac Q2\) 步内到达极大值,初始点一定要在前 \(\frac Q{12}\) 大的点中。成功概率为:
用 python 算一下大约是 \(97.6\%\)。
事实上上述式子根本跑不满,如果认为一次转移期望 \(3\) 次查询,就有 \(99.94\%\) 的正确率。事实上正确率比这个更高。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
#include<random>
#include<chrono>
#include<cmath>
using namespace std;
int qry(int x,int y,int z){printf("? %d %d %d\n",x,y,z);fflush(stdout);int r;scanf("%d",&r);return r;}
int n,m,k,Q;
mt19937 Rand(std::chrono::system_clock::now().time_since_epoch().count());
int range(int l,int r){return Rand()%(r-l+1)+l;}
namespace c1{
const int N=1000010;
int a[N],qt=0;
int qry(int x){if(a[x]) return a[x];++qt;int r=0;printf("? %d 1 1\n",x);fflush(stdout);scanf("%d",&r);return a[x]=r;}
int getl(int l,int r){return l+(r-l+1)*0.38;}
int getr(int l,int r){return r-(r-l+1)*0.38;}
void solve(int l,int r,int p=0)
{
if(l==r){printf("! %d 1 1\n",l);cerr<<qt<<endl;fflush(stdout);exit(0);}
if(r-l<=5)
{
int mid=(l+r)>>1;
if(qry(mid)>qry(mid+1)) solve(l,mid);
else solve(mid+1,r);
}
if(!p) p=getl(l,r);
int mid=(l+r)>>1,u=0;
if(p>=mid) u=getr(l,p);
else u=getl(p,r),swap(u,p);
if(qry(u)<qry(p)) solve(u+1,r,p);
else solve(l,p-1,u);
}
}
// int a[110][110];
namespace c2{
const int N=1010;
#define MP make_pair
int mp[N][N],mx,my;
void upd(int x,int y)
{
if(mp[x][y]<=mp[mx][my]) return;
mx=x,my=y;
}
int qry(int x,int y)
{
if(x<=0 || y<=0 || x>n || y>m) return 0;
if(mp[x][y]) return mp[x][y];
int r=0;
printf("? %d %d 1\n",x,y);fflush(stdout);scanf("%d",&r);
// printf("%d\n",r);
mp[x][y]=r;
upd(x,y);
return r;
}
int answer(int x,int y)
{
printf("! %d %d 1\n",x,y);
cerr<<mx<<" "<<my<<endl;
cerr<<x<<" "<<y<<endl;
if(mx!=x || my!=y) throw;
fflush(stdout);exit(0);
}
void solve(int xl,int xr,int yl,int yr)
{
if(xl==xr && yl==yr) answer(xl,yl);
if(xr-xl>=yr-yl)
{
int xm=(xl+xr)>>1;
int mw=0,p=0;
for(int i=yl;i<=yr;i++)
{
int w=qry(xm,i);
if(w>mw) mw=w,p=i;
}
qry(xm+1,p);qry(xm-1,p);
if(mx==xm) answer(xm,p);
if(mx>xm) solve(xm+1,xr,yl,yr);
else solve(xl,xm-1,yl,yr);
}
else
{
int ym=(yl+yr)>>1;
int mw=0,p=0;
for(int i=xl;i<=xr;i++)
{
int w=qry(i,ym);
if(w>mw) mw=w,p=i;
}
qry(p,ym+1);qry(p,ym-1);
if(my==ym) answer(p,ym);
if(my>ym) solve(xl,xr,ym+1,yr);
else solve(xl,xr,yl,ym-1);
}
}
}
namespace c3{
const int N=110;
int mp[N][N][N],mx,my,mz;
void upd(int x,int y,int z)
{
if(mp[x][y][z]<=mp[mx][my][mz]) return;
mx=x,my=y,mz=z;
}
int qry(int x,int y,int z)
{
if(x<=0 || y<=0 || z<=0 || x>n || y>m || z>k) return 0;
if(mp[x][y][z]) return mp[x][y][z];
int r=0;
printf("? %d %d %d\n",x,y,z);fflush(stdout);scanf("%d",&r);
// printf("%d\n",r);
mp[x][y][z]=r;
upd(x,y,z);
return r;
}
int answer(int x,int y,int z)
{
printf("! %d %d %d\n",x,y,z);
fflush(stdout);exit(0);
}
void solve(int xl,int xr,int yl,int yr,int zl,int zr)
{
if(xl==xr && yl==yr && zl==zr) answer(xl,yl,zl);
if(xr-xl>=yr-yl && xr-xl>=zr-zl)
{
int xm=(xl+xr)>>1;
int mw=0,py=0,pz=0;
for(int i=yl;i<=yr;i++)
for(int j=zl;j<=zr;j++)
{
int w=qry(xm,i,j);
if(w>mw) mw=w,py=i,pz=j;
}
qry(xm+1,py,pz);qry(xm-1,py,pz);
if(mx==xm) answer(xm,py,pz);
if(mx>xm) solve(xm+1,xr,yl,yr,zl,zr);
else solve(xl,xm-1,yl,yr,zl,zr);
}
else if(yr-yl>=xr-xl && yr-yl>=zr-zl)
{
int ym=(yl+yr)>>1;
int mw=0,px=0,pz=0;
for(int i=xl;i<=xr;i++)
for(int j=zl;j<=zr;j++)
{
int w=qry(i,ym,j);
if(w>mw) mw=w,px=i,pz=j;
}
qry(px,ym+1,pz);qry(px,ym-1,pz);
if(my==ym) answer(px,ym,pz);
if(my>ym) solve(xl,xr,ym+1,yr,zl,zr);
else solve(xl,xr,yl,ym-1,zl,zr);
}
else
{
int zm=(zl+zr)>>1;
int mw=0,px=0,py=0;
for(int i=xl;i<=xr;i++)
for(int j=yl;j<=yr;j++)
{
int w=qry(i,j,zm);
if(w>mw) mw=w,px=i,py=j;
}
qry(px,py,zm+1);qry(px,py,zm-1);
if(mz==zm) answer(px,py,zm);
if(mz>zm) solve(xl,xr,yl,yr,zm+1,zr);
else solve(xl,xr,yl,yr,zl,zm-1);
}
}
}
namespace all{
struct node{
int x,y,z;
node(int X=0,int Y=0,int Z=0):x(X),y(Y),z(Z){}
bool operator <(const node a)const{return x==a.x?(y==a.y?z<a.z:y<a.y):x<a.x;}
};
const int N=1010;
map<node,int>mp;
int mx,my,mz;
void upd(int x,int y,int z)
{
if(mp[node(x,y,z)]<=mp[node(mx,my,mz)]) return;
mx=x,my=y,mz=z;
}
int answer(int x,int y,int z)
{
printf("! %d %d %d\n",x,y,z);
fflush(stdout);exit(0);
}
int qry(int x,int y,int z)
{
if(x<=0 || y<=0 || z<=0 || x>n || y>m || z>k) return 0;
if(mp[node(x,y,z)]) return mp[node(x,y,z)];
int r=0;
printf("? %d %d %d\n",x,y,z);fflush(stdout);scanf("%d",&r);
// printf("%d\n",r);
mp[node(x,y,z)]=r;
upd(x,y,z);
return r;
}
void init()
{
int t=Q/2;
while(t --> 0)
{
int x=range(1,n),y=range(1,m),z=range(1,k);
qry(x,y,z);Q--;
}
}
node find_bigger(node a)
{
int t=qry(a.x,a.y,a.z);
if(qry(a.x+1,a.y,a.z)>t) return node(a.x+1,a.y,a.z);
if(qry(a.x,a.y+1,a.z)>t) return node(a.x,a.y+1,a.z);
if(qry(a.x,a.y,a.z+1)>t) return node(a.x,a.y,a.z+1);
if(qry(a.x-1,a.y,a.z)>t) return node(a.x-1,a.y,a.z);
if(qry(a.x,a.y-1,a.z)>t) return node(a.x,a.y-1,a.z);
if(qry(a.x,a.y,a.z-1)>t) return node(a.x,a.y,a.z-1);
answer(a.x,a.y,a.z);exit(0);
}
void solve()
{
init();
node a(mx,my,mz);
while(true) a=find_bigger(a);
}
}
int main()
{
scanf("%d%d%d%d",&n,&m,&k,&Q);
if(m==1 && k==1) c1::solve(1,n);
if(k==1) c2::solve(1,n,1,m);
// c3::solve(1,n,1,m,1,k);
all::solve();
return 0;
}
E. Genetics
首先显然有一个 \(O(\frac{n^3}{\omega})\) 的做法,卡一卡常在 LOJ 上能过。这里考虑一个高明的 \(O(n^2)\) 做法:
定义每个字符串长度为 \(m\)。
如果我们定义第 \(i\) 个字符串价值是 \(m^i\),定义 \(f(i,j)\) 表示第 \(i\) 个字符串与第 \(j\) 个字符串不同字符个数乘上 \(m^j\),要对于每个 \(i\) 求 \(g_i=\sum f(i,j)\)。显然只要对每个位置的某个字符统计贡献即可 \(O(n)\) 计算。
可以发现 \(g_i=k\sum_{j\neq i}m^j\) 当且仅当字符串 \(i\) 是要找的那个串。
事实上并不需要权值为 \(m^i\),只需要钦定一个随机价值即可。可以证明上述做法在 long long 范围内随机的错误概率大约是 \(2^{-50}\)。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<algorithm>
#include<chrono>
#include<random>
#include<cmath>
#define N 4100
#define ll long long
using namespace std;
char s[N][N];
int pos(char c){return c=='A'?0:(c=='C'?1:(c=='G'?2:3));}
mt19937 Rand(std::chrono::system_clock::now().time_since_epoch().count());
ll p[N],f[N][4];
int main()
{
int n,m,k;scanf("%d%d%d",&n,&m,&k);
bool only=true;ll sum=0;
for(int i=0;i<n;i++)
{
scanf("%s",s[i]),p[i]=Rand(),sum+=p[i];
for(int j=0;j<m;j++) f[j][pos(s[i][j])]-=p[i];
}
for(int i=0;i<m;i++) for(int j=0;j<4;j++) f[i][j]+=sum;
for(int i=0;i<n;i++)
{
ll res=0;for(int j=0;j<m;j++) res+=f[j][pos(s[i][j])];
if(res==k*(sum-p[i])){printf("%d\n",i+1);return 0;}
}
return 0;
}
F. Paths
签到题。
令 \(f_{i,S}\) 表示到点 \(i\),其中 \(S\) 子集的颜色已经走过的方案数。直接 dp 即可。复杂度 \(O(m2^k)\)。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define N 300010
#define ll long long
#define M 5
using namespace std;
int col[N];
vector<int>g[N];
ll f[N][1<<M];
int main()
{
int n,m,k;scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++) scanf("%d",&col[i]),col[i]--;
for(int i=1,u,v;i<=m;i++) scanf("%d%d",&u,&v),g[u].push_back(v),g[v].push_back(u);
for(int i=1;i<=n;i++) f[i][1<<col[i]]=1;
ll ans=0;
for(int s=1;s<1<<k;s++)
{
for(int u=1;u<=n;u++) if(f[u][s])
for(int v:g[u]) if(!(s>>col[v]&1)) f[v][s^(1<<col[v])]+=f[u][s];
}
for(int u=1;u<=n;u++)
for(int s=1;s<1<<k;s++) ans+=f[u][s];
printf("%lld\n",ans-n);
return 0;
}

浙公网安备 33010602011771号