#5 CF555E & CF566E & CF568C
Case of Computer Network
题目描述
解法
显然本题是一个边双连通分量版题,缩点之后树上差分定向即可。由于我以前没有怎么写过点双和边双,所以我的主要目的是把它们总结一下。
点双:在强连通分量的基础上,不在回溯的时候染色,而是在访问完某个儿子之后立即判断 low[v]>=dfn[u]
,如果是的话就立即划分点双连通分量,并且把 \(u\) 这个点也放进连通分量中。并且如果根只有一个儿子需要特判,此时根不是割点。
边双:在 \(\tt tarjan\) 的时候不进行染色,只是回溯时对于满足 low[v]>dfn[u]
的边设置为不可访问的。然后暴力 \(\tt dfs\),每次将访问到的点全部设置为同一个边双,不需要任何特判。
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <iostream>
using namespace std;
const int M = 200005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,tot,f[M],vis[M<<1],col[M],bl[M],dep[M];
int t,cnt,dfn[M],low[M],a[M],b[M],fa[M][20];
struct edge{int v,next;}e[M<<1];vector<int> g[M];
void tarjan(int u,int fa)
{
dfn[u]=low[u]=++cnt;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa) continue;
if(!dfn[v])
{
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u])
vis[i]=vis[i^1]=1;
}
else low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u,int c)
{
col[u]=c;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(col[v] || vis[i]) continue;
dfs(v,c);
}
}
void pre(int u,int p)
{
bl[u]=bl[p];fa[u][0]=p;
dep[u]=dep[p]+1;
for(int i=1;i<20;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int v:g[u]) if(v^p)
pre(v,u);
}
int lca(int u,int v)
{
if(dep[u]<dep[v]) swap(u,v);
for(int i=19;i>=0;i--)
if(dep[fa[u][i]]>=dep[v])
u=fa[u][i];
if(u==v) return u;
for(int i=19;i>=0;i--)
if(fa[u][i]^fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
void pd(int u)
{
for(int v:g[u]) if(v^fa[u][0])
pd(v),a[u]+=a[v],b[u]+=b[v];
if(a[u] && b[u]) {puts("No");exit(0);}
}
signed main()
{
n=read();m=read();k=read();tot=1;
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
e[++tot]=edge{u,f[v]},f[v]=tot;
e[++tot]=edge{v,f[u]},f[u]=tot;
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,0);
for(int i=1;i<=n;i++)
if(!col[i]) dfs(i,col[i]=++t);
for(int u=1;u<=n;u++)
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(col[u]<col[v])
{
g[col[u]].push_back(col[v]);
g[col[v]].push_back(col[u]);
}
}
for(int i=1;i<=t;i++)
if(!bl[i]) bl[0]=i,pre(i,0);
while(k--)
{
int u=col[read()],v=col[read()];
if(bl[u]!=bl[v])
{
puts("No");
return 0;
}
if(u==v) continue;
int x=lca(u,v);
a[u]++;a[x]--;b[v]++;b[x]--;
}
for(int i=1;i<=t;i++)
if(!fa[i][0]) pd(i);
puts("Yes");
}
Restoring Map
题目描述
解法
真正的思维不用言语过多的表达,当我看其他直接给结论的题解一头雾水时,这篇博客仅仅用一些简单的讨论就惊醒梦中人。
距离,距离,本质上是两点之间关系。所以我们先对两点 \(x,y\) 之间的关系进行大致的考察,由于两点之间的交集仍然是连通块,可以考察它们交集的性质:
- 距离 \(>4\),交集为空。
- 距离 \(=4\),交集大小为 \(1\)
- 距离 \(=3\),交集大小为 \(2\)
- 距离 \(=2\),交集大小 \(\geq 3\)
- 距离 \(=1\),交集大小取决于 \(n\),当 \(n=2\) 时交集为 \(2\),否则 \(\geq 3\)
在特判掉一些边界情况之后(菊花图、\(n=2\)),发现距离 \(=3\) 的两个点的交集正好能描述一条边。并且发现有这样的结论:非叶节点 \(x,y\) 之间有边,当且仅当存在两个点交集为 \(\{x,y\}\)
上述结论的充分性必要性都不难证,大讨论即可,我在这里写出来意义不大。
所以我们可以枚举两个点 \((x,y)\),然后用 \(\tt bitset\) 求他们的交集,这样我们可以得到所有非叶节点构成的树,然后特判掉非叶节点个数为 \(2\) 的情况。
那么现在要对每个叶子找出他的父亲,首先我们可以知道每个叶子对应的集合,就是包含它的,并且大小最小的集合。发现把集合中所有叶节点去掉后,这个集合就是父亲在非叶节点树上的连边集合,这可以作为我们找父亲的判据。
那么全程用 \(\tt bitset\) 优化,时间复杂度 \(O(\frac{n^3}{w})\)
总结
从情况较少的地方分类,本题得到结论的另一个动机是:集合距离 \(\leq 2\),所以关于两点的集合若是有交集,那么距离的情况数是很少的。
#include <cstdio>
#include <bitset>
using namespace std;
const int M = 1005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,cnt,v[M],w[M];bitset<M> a[M],b[M],e[M];
signed main()
{
n=read();int fl=1;
if(n==2) {puts("1 2");return 0;}
for(int i=1;i<=n;i++)
{
int k=read();
if(k!=n) fl=0;
while(k--) a[i][read()]=1;
}
if(fl)
{
for(int i=2;i<=n;i++)
printf("%d %d\n",1,i);
return 0;
}
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
{
bitset<M> o=a[i]&a[j];
if(o.count()!=2) continue;
int x=o._Find_first();
int y=o._Find_next(x);
if(e[x][y]) continue;
printf("%d %d\n",x,y);
e[x][y]=e[y][x]=1;v[x]=v[y]=1;
}
for(int i=1;i<=n;i++)
{
if(v[i]) {e[i][i]=1;cnt++;continue;}
int s=n+1,o=0;
for(int j=1;j<=n;j++)
if(!w[j] && a[j][i])
{
int c=a[j].count();
if(c<s) s=c,o=j;
}
b[i]=a[o];w[o]=1;
}
if(cnt==2)
{
int x=0,y=0;
for(int i=1;i<=n;i++)
if(v[i]) !x?(x=i):(y=i);
for(int i=1;i<=n;i++) if(a[i].count()<n)
{
for(int j=1;j<=n;j++)
{
if(v[j]) continue;
printf("%d ",j);
printf("%d\n",(a[i][j])?x:y);
}
break;
}
return 0;
}
for(int i=1;i<=n;i++) if(!v[i])
{
for(int j=1;j<=n;j++)
if(!v[j]) b[i][j]=0;
for(int j=1;j<=n;j++)
if(v[j] && b[i]==e[j])
{
printf("%d %d\n",i,j);
break;
}
}
}
New Language
题目描述
解法
这次不幸遇到大佬题解写错的情况,我因此空耗了好久时间。以后还是要保持独立思考的能力,如果觉得题解哪里写错了要充分相信自己,此时做好的做法不是想法设法解释题解,而是找出 \(\tt hack\) 数据!
如果有人会 \(O(nm)\) 的做法请联系我,我只能讲解 \(O(n^2m)\) 的做法。
我对 \(\tt2sat\) 这东西真不能说是很熟悉。首先我们考虑一个简化的问题,如何求出原问题字典序最小的解?方法很简单,我们先搜索字典序小的,再搜索字典序大的,如果在当前局面下不出现矛盾(指 \(x,inv(x)\) 都被选取,\(inv(x)\) 指 \(x\) 对应的取反点),就可以去考虑下一位。
可以证明这样来说明它的正确性:考虑会出现矛盾的情况是某个点 \(x\) 能走到 \(y\),但是 \(inv(y)\) 在前面已经被选取了,此时选取 \(x\) 会出现矛盾?考虑对称性 \(inv(y)\) 可以走到 \(inv(x)\),所以 \(inv(x)\) 会被强制选取这一位就不需要决策了,那么我们可以说明如果原来有解这样构造一定有解。
\(\tt UPD\):为了更好的证明这里的结论最好表述成如果在第 \(i\) 位无解,那么改变前 \(i-1\) 位的取值是没有作用的。因为 \(x\) 能到达的范围都和前 \(i-1\) 位是没关系的,所以改变取值是没有用的。
我们尝试把问题转化成最小字典序的问题,我们枚举一个前缀和原来的 \(s\) 相同,然后强制下一个位置大于 \(s\),那么剩下位置的限制就可以直接取消了,暴力 \(\tt dfs\) 时间复杂度 \(O(n^2m)\)
#include <cstdio>
#include <vector>
#include <cstring>
using namespace std;
const int M = 1005;
#define Morisummer {printf("%s",s+1);return 0;}
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,tp,a[M],b[M][2],v[M],st[M];char s[M];
vector<int> g[M];
int inv(int u) {return u>n?u-n:u+n;}
void add(int u,int v) {g[u].push_back(v);}
int dfs(int u)
{
if(v[inv(u)]) return 0;
v[u]=1;st[++tp]=u;
for(int x:g[u])
if(!v[x] && !dfs(x))
return 0;
return 1;
}
int pd(int m)
{
tp=0;
for(int i=1;i<=2*n;i++) v[i]=0;
for(int i=1;i<=m;i++)
if(!dfs(i+a[s[i]-'a'+1]*n))
return 0;
for(int i=m+1;i<=n;i++)
{
if(v[i]) s[i]=b[1][0]+'a'-1;
else if(v[i+n]) s[i]=b[1][1]+'a'-1;
else
{
int x=min(b[1][0],b[1][1]);
int y=max(b[1][0],b[1][1]);tp=0;
if(dfs(i+a[x]*n)) s[i]=x+'a'-1;
else
{
while(tp) v[st[tp--]]=0;
if(dfs(i+a[y]*n)) s[i]=y+'a'-1;
else return 0;
}
}
}
return 1;
}
signed main()
{
scanf("%s",s+1);k=strlen(s+1);
b[k+1][0]=b[k+1][1]=k+1;
for(int i=k,x=k+1,y=k+1;i>=1;i--)
{
if(s[i]=='V') x=i,a[i]=0;
if(s[i]=='C') y=i,a[i]=1;
b[i][0]=x;b[i][1]=y;
}
n=read();m=read();
for(int i=1;i<=m;i++)
{
int x=read();scanf("%s",s+1);
if(s[1]=='C') x+=n;
int y=read();scanf("%s",s+1);
if(s[1]=='C') y+=n;
add(x,y);add(inv(y),inv(x));
}
scanf("%s",s+1);
if(pd(n)) Morisummer
else if(b[1][0]==k+1 || b[1][1]==k+1)
{puts("-1");return 0;}//can't change
for(int i=n;i>=1;i--)
{
int c=s[i]-'a'+2;
int x=min(b[c][0],b[c][1]);
int y=max(b[c][0],b[c][1]);
if(x!=k+1)
{
s[i]=x+'a'-1;
if(pd(i)) Morisummer
}
if(y!=k+1)
{
s[i]=y+'a'-1;
if(pd(i)) Morisummer
}
}
puts("-1");
}