#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"); 
}
posted @ 2022-02-17 19:55  C202044zxy  阅读(418)  评论(2编辑  收藏  举报