POI2013题解
只做了BZ上有的\(13\)道题。
就这样还扔了两道神仙构造和一道计算几何题。所以只剩下十道题了。
[BZOJ3414][Poi2013]Inspector
肯定是先二分答案,然后每个人的出现区间至少要包含于他自己记录的所有时间点。如果某个人没有记录过那他的出现区间任意。
从左往右扫描,维护以下几个东西:
\(t\):目前还有多少人的区间不确定。
\(s\):当前要求选多少人。(这个是由记录者决定的)
\(cl\):有多少人的区间可以向左扩展。
\(cr\):有多少人的区间可以向右扩展。
每扫到一个位置,如果\(s>\)记录的人数则无解,否则\(s+cl+cr\)就是当前可存在于该位置上的人数,若人多了就优先减\(cr\)再减\(cl\),若人少了就从\(t\)中拿人补到\(cl\)中。
#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 1e5+5;
int n,m,tim[N],peo[N],rec[N],L[N],R[N],num[N],pl[N],pr[N];
bool check(int mid){
for (int i=1;i<=n;++i) L[i]=N,R[i]=0;
for (int i=1;i<=m;++i) num[i]=pl[i]=pr[i]=0;
for (int i=1;i<=mid;++i){
int t=tim[i],p=peo[i],r=rec[i];
if (num[t]&&num[t]!=r) return false;
num[t]=r;L[p]=min(L[p],t);R[p]=max(R[p],t);
}
for (int i=1;i<=n;++i) if (R[i]) ++pl[L[i]],++pr[R[i]];
for (int i=1,t=n,s=0,cl=0,cr=0;i<=m;++i)
if (num[i]){
s+=pl[i];
while (pl[i]--) cl?--cl:--t;
if (s>num[i]) return false;
while (s+cl+cr<num[i]) ++cl,--t;
while (s+cl+cr>num[i]) cr?--cr:--cl;
s-=pr[i];cr+=pr[i];
if (t<0) return false;
}
return true;
}
int main(){
int Case=gi();while (Case--){
n=gi();m=gi();
for (int i=1;i<=m;++i) tim[i]=gi(),peo[i]=gi(),rec[i]=gi()+1;
int l=0,r=m,res=0;
while (l<=r){
int mid=l+r>>1;
if (check(mid)) res=mid,l=mid+1;
else r=mid-1;
}
printf("%d\n",res);
}
return 0;
}
[BZOJ3415][Poi2013]Price List
可知有且仅有三种策略:
1、只走\(a\)边,可以当做\(b\)边不存在。
2、把两条\(a\)边并做一条\(b\)边走。
3、一条\(a\)边都不会走。
前两种直接在原图中\(bfs\)即可出解。
考虑第三种,如果在\(bfs\)的过程中对于每个点\(x\)枚举所有与他距离为\(2\)的点,复杂度会退化成\(O(m^2)\)。
假设我们有两个边集\(G_0,G_1\),初始时都为原图。从队列中取出一个点\(u\),先枚举其在\(G_0\)中的所有出边指向的点\(v\)并标记,再顺次枚举每个\(v\)在\(G_1\)中的出边指向的点\(w\),如果\(w\)未被标记(\(u,v,w\)三点不构成三元环)则可用\(u\)更新\(w\),更新后把\(G_1\)中\(v\to w\)的这条边删除。
在不考虑三元环的前提下,\(G_0\)每条边被访问一起,\(G_1\)每条边也被访问一次,所以复杂度线性。但是构成三元环会导致边\(v\to w\)不能删。而三元环的数量是\(O(m\sqrt m)\)的,每个三元环会给复杂度带来一个常数(会被访问\(3\)次),所以复杂度为\(O(m\sqrt m)\)。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 2e5+5;
struct Graph{
int to[N],nxt[N],hd[N],cnt;
void link(int u,int v){
to[++cnt]=v;nxt[cnt]=hd[u];hd[u]=cnt;
to[++cnt]=u;nxt[cnt]=hd[v];hd[v]=cnt;
}
}G,F;
int n,m,S,A,B,dep[N],vis[N],ans[N];queue<int>Q;
int main(){
n=gi();m=gi();S=gi();A=gi();B=gi();
for (int i=1;i<=m;++i){
int u=gi(),v=gi();
G.link(u,v);F.link(u,v);
}
dep[S]=1;Q.push(S);
while (!Q.empty()){
int u=Q.front();Q.pop();
for (int e=G.hd[u];e;e=G.nxt[e]){
int v=G.to[e];
if (!dep[v]) dep[v]=dep[u]+1,Q.push(v);
}
}
for (int i=1;i<=n;++i) --dep[i],ans[i]=min(dep[i]*A,(dep[i]>>1)*B+(dep[i]&1)*A);
memset(dep,0,sizeof(dep));dep[S]=1;Q.push(S);
while (!Q.empty()){
int u=Q.front();Q.pop();
for (int e=G.hd[u];e;e=G.nxt[e]) vis[G.to[e]]=u;
for (int e=G.hd[u];e;e=G.nxt[e]){
int v=G.to[e];
for (int i=F.hd[v],las=0;i;i=F.nxt[i]){
int w=F.to[i];
if (vis[w]==u) {las=i;continue;}
if (!dep[w]) dep[w]=dep[u]+1,Q.push(w);
if (!las) F.hd[v]=F.nxt[F.hd[v]];
else F.nxt[las]=F.nxt[i];
}
}
}
for (int i=1;i<=n;++i) if (dep[i]) --dep[i],ans[i]=min(ans[i],dep[i]*B);
for (int i=1;i<=n;++i) printf("%d\n",ans[i]);
return 0;
}
[BZOJ3416][Poi2013]Take-out
从前往后依次压进栈中,如果栈内的前\(k+1\)个元素中恰好有一个白的就把这\(k+1\)个作为一组方案。倒序输出即可。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e6+5;
int n,k,q[N],sum[N],top,ans[N],cnt;char s[N];
int main(){
scanf("%d%d%s",&n,&k,s+1);++k;
for (int i=1;i<=n;++i){
q[++top]=i;sum[top]=sum[top-1]+(s[i]=='c');
if (top>=k&&sum[top]-sum[top-k]==1)
for (int tt=top-k;top>tt;--top)
ans[++cnt]=q[top];
}
for (int i=n;i;--i) printf("%d%c",ans[i],i%k==1?'\n':' ');
return 0;
}
[BZOJ3417][Poi2013]Tales of seafaring
\(O(n^2)\)预处理出\(i\)到\(j\)走奇数/偶数步的最短路。需要特判孤立点走大于零的偶数步到达自身是不合法的。
有人说预处理存不下?最短路长度最多\(2n\)你开个\(\mbox{short}\)存不就好啦?
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 5005;
short n,m,to[N<<1],nxt[N<<1],head[N],cnt,q[N<<1],hd,tl,dep[N][N<<1];
int k;
int main(){
n=gi();m=gi();k=gi();
for (short i=1;i<=m;++i){
short u=gi(),v=gi();
to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;
to[++cnt]=u;nxt[cnt]=head[v];head[v]=cnt;
}
memset(dep,-1,sizeof(dep));
for (short i=1;i<=n;++i){
dep[i][i<<1]=0;q[hd=tl=1]=i<<1;
while (hd<=tl){
short x=q[hd++],u=x>>1,c=(x&1)^1;
for (short e=head[u];e;e=nxt[e])
if (!~dep[i][to[e]<<1|c])
dep[i][to[e]<<1|c]=dep[i][x]+1,q[++tl]=to[e]<<1|c;
}
}
while (k--){
short u=gi(),v=gi();int d=gi();
if (u==v&&!head[u]&&d>0) puts("NIE");
else if (dep[u][v<<1|(d&1)]!=-1&&(int)dep[u][v<<1|(d&1)]<=d) puts("TAK");
else puts("NIE");
}
return 0;
}
[BZOJ3419][Poi2013]Taxis
首先可以发现的是,从大到小叫车是最优的,而且如果已行走距离超过了\(d\)就只需要一辆车就可以到终点了。
先从大到小叫车,这样如果有解的话一定是最优解,但是可能会无解,就比如说把所有\(\ge m-d\)的车全耗在了前半段导致后半段过不去。还是贪心,选出\(\ge m-d\)的最小的留给后半段,剩下的还是贪心选。这样就能得到答案了。
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
ll gi(){
ll x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 5e5+5;
int n,p,ans;ll m,d,a[N],now;
bool cmp(ll i,ll j){return i>j;}
void cal(ll x){if(now<d)now+=max(0ll,x-d+now);else now=max(now,d+x);}
int main(){
m=gi();d=gi();n=gi();
for (int i=1;i<=n;++i) a[i]=gi();
sort(a+1,a+n+1,cmp);
for (int i=1;i<=n;++i){
cal(a[i]);if (now>=m) {ans=i;break;}
if (a[i]>=m-d) p=i;
}
if (!ans){
now=0;ll pos=d-(a[p]-m+d>>1);
for (int i=1;i<=n;++i)
if (i^p){
cal(a[i]);if (now>=pos) {ans=i;break;}
}
}
printf("%d\n",ans);return 0;
}
[BZOJ3420][Poi2013]Triumphal arch
先二分答案,设\(f_i\)表示在\(i\)节点至少需要预先在\(i\)子树内标记多少个点,转移\(f_i=\max\{0,\sum_j (f_j+1)-k\}\)。
#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 6e5+5;
int n,to[N],nxt[N],head[N],cnt;
int dfs(int u,int f,int k){
int s=0;
for (int e=head[u];e;e=nxt[e])
if (to[e]!=f) s+=dfs(to[e],u,k)+1;
return max(0,s-k);
}
int main(){
n=gi();
for (int i=1;i<n;++i){
int u=gi(),v=gi();
to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;
to[++cnt]=u;nxt[cnt]=head[v];head[v]=cnt;
}
int l=0,r=n-1,res=0;
while (l<=r){
int mid=l+r>>1;
if (!dfs(1,0,mid)) res=mid,r=mid-1;
else l=mid+1;
}
printf("%d\n",res);return 0;
}
[BZOJ3421][Poi2013]Walk
有一个结论:在一个\(n\)维超立方体上有\(2^n\)个点,如果把这\(2^n\)个点分成\(2\)个点集,那么这样个点集之间的边数至少为两个点集的点数较小值。根据这个假设是已知的结论就可以证明一个定理:删除\(k\)个点后,图中存在至多一个大小大于\(nk\)的连通块,太懒了就不在这里证了。
所以直接\(bfs\),从\(S\)出发只扩展至多\(nk\)个状态,如果两边搜出了超过\(nk\)个状态还没有相遇就说明两点都在那个大于\(nk\)的连通块里面。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define ll long long
const int N = 5e6+500;
const int mod = 5170427;
int n,k,nk,hd,tl,nxt[N],head[mod],cnt;ll a[N>>2],q[N],to[N];
char s[100];
ll read(){
scanf("%s",s);ll res=0;
for (int i=0;i<n;++i) res=res<<1|s[i]-'0';
return res;
}
void add1(ll x){
to[++cnt]=x;nxt[cnt]=head[x%mod];head[x%mod]=cnt;
}
void add2(ll x){
for (int e=head[x%mod];e;e=nxt[e])
if (to[e]==x) return;
q[++tl]=to[++cnt]=x;nxt[cnt]=head[x%mod];head[x%mod]=cnt;
}
void work(ll S,ll T){
memset(head,0,sizeof(head));cnt=0;
hd=1;tl=0;add2(S);
for (int i=1;i<=k;++i) add1(a[i]);
while (hd<=tl&&tl<=nk){
ll u=q[hd++];if (u==T) puts("TAK"),exit(0);
for (int i=0;i<n;++i) add2(u^(1ll<<i));
}
if (tl<=nk) puts("NIE"),exit(0);
}
int main(){
scanf("%d%d",&n,&k);nk=n*k;
ll S=read(),T=read();
for (int i=1;i<=k;++i) a[i]=read();
work(S,T);work(T,S);
puts("TAK");return 0;
}
[BZOJ3425][Poi2013]Polarization
最小答案一定是\(n-1\),只要按边深度的奇偶性取不同的方向就可以取到了。
最大答案存在一个结论,就是以重心为根,剩下每棵子树要么全部指向根,要么全部背离根。假设指向根的子树大小之和是\(x\),那么背离根的子树大小之和就是\(n-x-1\),所有穿过根的贡献就是\(x(n-x-1)\)。
所以现在要做的就是拿重心的所有儿子出来做一个\(01\)背包看可以组成哪些\(x\)。直接背包压位的话复杂度是\(O(\frac{n^2}{32})\),然后把\(\ge \sqrt n\)的暴力做,\(< \sqrt n\)的放在一起二进制分组做,复杂度就是\(O(\frac{n\sqrt n}{32})\)。
#include<cstdio>
#include<algorithm>
#include<bitset>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
#define ll long long
const int N = 250005;
int n,to[N<<1],nxt[N<<1],head[N],cnt,sz[N],w[N],rt,s[505];
ll f[N];bitset<N>g;
void dfs(int u,int fa){
sz[u]=1;w[u]=f[u]=0;
for (int e=head[u],v;e;e=nxt[e])
if ((v=to[e])!=fa){
dfs(v,u);sz[u]+=sz[v];
f[u]+=f[v]+sz[v];w[u]=max(w[u],sz[v]);
}
w[u]=max(w[u],n-sz[u]);if (w[u]<w[rt]) rt=u;
}
int main(){
n=gi();if (n==1) return puts("0 0"),0;
for (int i=1;i<n;++i){
int u=gi(),v=gi();
to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;
to[++cnt]=u;nxt[cnt]=head[v];head[v]=cnt;
}
w[0]=n;dfs(1,0);dfs(rt,0);g[0]=1;
for (int e=head[rt];e;e=nxt[e])
if (sz[to[e]]>=500) g|=g<<sz[to[e]];
else ++s[sz[to[e]]];
for (int i=1;i<500;++i)
for (int j=s[i],k=1;j;j-=k,k=min(j,k<<1))
g|=g<<i*k;
for (int i=n>>1;i;--i)
if (g[i]) return printf("%d %lld\n",n-1,f[rt]+1ll*i*(n-i-1)),0;
}
[BZOJ3426][Poi2013]Tower Defense Game
直接每次选一个没覆盖的点就行了,因为这样做一定可以包含至少一个最优方案中的选择点。
至于维护这个覆盖,对每个点记一下最近的选择点到他的距离(代码中有点不一样,没关系啦),覆盖时如果不能更新这个距离就不走了(因为之前肯定已经走过了)。考虑到每个点的这个距离只会被更新常数次,所以复杂度是对的。
#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 2e6+5;
int n,m,to[N],nxt[N],head[N],cnt,vis[N],q[N],ans;
void dfs(int u,int c){
vis[u]=c;
for (int e=head[u];e;e=nxt[e])
if (vis[to[e]]<c-1) dfs(to[e],c-1);
}
int main(){
n=gi();m=gi();gi();
for (int i=1;i<=m;++i){
int u=gi(),v=gi();
to[++cnt]=v;nxt[cnt]=head[u];head[u]=cnt;
to[++cnt]=u;nxt[cnt]=head[v];head[v]=cnt;
}
for (int i=1;i<=n;++i) if (!vis[i]) q[++ans]=i,dfs(i,3);
printf("%d\n",ans);
for (int i=1;i<=ans;++i) printf("%d ",q[i]);
puts("");return 0;
}
[BZOJ3427][Poi2013]Bytecomputer
猜个结论最终序列一定是一段\(-1\)一段\(0\)一段\(1\)?
这样以来就直接\(dp\)一下就好了。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int gi(){
int x=0,w=1;char ch=getchar();
while ((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if (ch=='-') w=0,ch=getchar();
while (ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return w?x:-x;
}
const int N = 1e6+5;
int n,f[N][3];
int main(){
memset(f,63,sizeof(f));
n=gi();f[1][gi()+1]=0;
for (int i=2;i<=n;++i){
int x=gi();
if (x==1){
f[i][2]=min(f[i-1][2],min(f[i-1][0],f[i-1][1]));
f[i][1]=f[i-1][0]+1;f[i][0]=f[i-1][0]+2;
}else if (x==0){
f[i][1]=min(f[i-1][1],f[i-1][0]);
f[i][2]=f[i-1][2]+1;f[i][0]=f[i-1][0]+1;
}else f[i][0]=f[i-1][0],f[i][2]=f[i-1][2]+2;
}
int ans=min(f[n][0],min(f[n][1],f[n][2]));
if (ans==f[0][0]) puts("BRAK");
else printf("%d\n",ans);
return 0;
}