破环为树

\(\text{[ZJOI 2008] }\)骑士

解法

题目实际上是求基环树上的最大点独立集的问题。对于一棵基环树,它的所有独立集方案数必然可以被边 \((u,v)\) 划分 —— 选择了 \(u\) 或选择了 \(v\)。这也是 "破环为树" 的基础。

于是可以随便选一条环上的边 \((u,v)\),枚举 \(u,v\) 作为根,且钦定根不能被选 —— 这实际上是忽略了这条边,我们可以用树形 \(\mathtt{dp}\) 得出答案。

题目中还有一个很好的性质是 "一个骑士只有一个最讨厌的骑士"。建树时不妨将最讨厌的骑士作为他的父亲,这样可以保证基环树的环一定在根那一坨。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f|=(s=='-');
	while(s>='0' and s<='9')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-'),write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
} 

#include <vector>
#include <iostream>
using namespace std;
typedef long long ll;

const int maxn=1e6+6;

vector <int> e[maxn];
int n,val[maxn],f[maxn],rt;
bool vis[maxn];
ll dp[maxn][2];

void dfs(int u) {
	vis[u]=1;
	dp[u][0]=0; dp[u][1]=val[u];
	for(auto v:e[u]) {
		if(v^rt) {
			dfs(v);
			dp[u][0]+=max(dp[v][0],dp[v][1]);
			dp[u][1]+=dp[v][0];
		}
	}
}

ll work(int x) {
	while(!vis[x]) {
		vis[x]=1;
		x=f[x];
	}
	dfs(rt=x); 
	ll tmp=dp[x][0];
	dfs(rt=f[x]);
	return max(tmp,dp[rt][0]);
}

int main() {
	n=read(9);
	for(int i=1;i<=n;++i) {
		val[i]=read(9);
		int x=read(9);
		e[x].push_back(i);
		f[i]=x;
	}
	ll ans=0;
	for(int i=1;i<=n;++i)
		if(!vis[i])
			ans+=work(i);
	print(ans,'\n');
	return 0;
}

\(\text{Card Game}\)

解法

对于一对 \((x,y)\),从 \(x\)\(y\) 连边。问题就变成了:翻转一条边的代价为 \(1\),求使所有点的出度至多为 \(1\) 的最小代价及其方案数。对于每个连通块可以分成三种情况讨论:

  • \(m>n\)。此时无解。
  • \(m=n-1\)。一定有一个点出度为 \(0\),不妨令那个点为根。同样,整棵树的边的方向也都确定了。换根 \(\mathtt{dp}\) 即可解决。
  • \(m=n\)。此时构成一棵基环树,由于环上的点都至少有 \(1\) 的出度,所以不在环上的点的边一定是朝着环上的,也就是固定的。环上的点有两种情况,对于环上点 \(u\),枚举出度由连接它的哪条边贡献。你会发现 \(u\) 类似于根,枚举的边其实是将它删去,所以也可以用一样的换根 \(\mathtt{dp}\)

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f|=(s=='-');
	while(s>='0' and s<='9')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-'),write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
} 

#include <vector>
#include <iostream>
using namespace std;

const int maxn=2e5+5,mod=998244353;

int n,head[maxn],cnt_d,cnt_e,cnt;
int st,en,ID,f[maxn],g[maxn];
bool vis[maxn];
struct edge {
	int nxt,to,id;
} e[maxn];
vector <int> res;

void addEdge(int u,int v,int i) {
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	e[cnt].id=i;
	head[u]=cnt;
}

void check(int u) {
	vis[u]=1; ++cnt_d;
	for(int i=head[u];i;i=e[i].nxt) {
		++cnt_e;
		if(!vis[e[i].to])
			check(e[i].to);
	}
}

int inc(int x,int y) {
	return x+y>=mod?x+y-mod:x+y;
}

void dfs(int u,int fa) {
	f[u]=0; vis[u]=1;
	for(int i=head[u];i;i=e[i].nxt) {
		int v=e[i].to;
		if(v==fa) continue;
		if(vis[v]) {
			st=u,en=v;
			ID=e[i].id;
		}
		else {
			dfs(v,u);
			f[u]=inc(f[u],inc(f[v],!(e[i].id&1)));
		}
	}
}

void dp(int u,int lst) {
	res.push_back(g[u]);
	for(int i=head[u];i;i=e[i].nxt) {
		if(i==lst or e[i].id==ID or e[i].id==(ID^1)) continue;
		int v=e[i].to;
		g[v]=inc(g[u],(e[i].id&1)?1:mod-1);
		dp(v,i^1);
	}
}

signed main() {
	for(int T=read(9);T;--T) {
		n=read(9);
		cnt=1;
		fill(&head[1],&head[n<<1]+1,0);
		fill(&vis[1],&vis[n<<1]+1,0);
		for(int i=1;i<=n;++i) {
			int u,v;
			u=read(9),v=read(9);
			addEdge(u,v,(i<<1)-2);
			addEdge(v,u,(i<<1)-1);
		}
		n<<=1;
		bool flag=0;
		for(int i=1;i<=n;++i) {
			if(vis[i]) continue;
			cnt_d=cnt_e=0;
			check(i);
			if((cnt_e>>1)>cnt_d) {
				flag=1; break;
			}
		}
		if(flag) {
			puts("-1 -1");
			continue;
		}
		fill(&vis[1],&vis[n]+1,0);
		int minval=0,plans=1,tmp;
		for(int i=1;i<=n;++i) {
			if(vis[i]) continue;
			st=en=ID=-1; tmp=0;
			dfs(i,0);
			g[i]=f[i];
			res.clear();
			dp(i,0);
			if(~st) {
				ID%=2;
				if(g[st]+ID==g[en]+(ID^1))
					tmp=2;
				else tmp=1;
				minval+=min(g[st]+ID,g[en]+(ID^1));
			}
			else {
				int mn=1e9;
				for(auto j:res)
					mn=min(mn,j);
				if(mn==1e9) continue;
				minval+=mn;
				for(auto j:res)
					if(j==mn) ++tmp;
			}
			plans=1ll*plans*tmp%mod;
		}
		printf("%d %d\n",minval,plans);
	}
	return 0;
}

在环上合并

\(\text{Island }\)岛屿

解法

对于每棵基环树,处理出所有在环上的点,在以这些点为根的子树中 \(\mathtt{dp}\) 出子树的直径以及 \(dp_i\) 表示经过根最长链的长度。接下来需要将环上的两个点拼起来。

破环为链(将环倍长),环上的点 \(x\) 可以这样更新:

\[\text{Ans}=\max\{dp_y+\text{dis}(x,y)\} \]

本来需要考虑 \(x\)\(y\) 在环上有两条路径,但由于破环为链,另一个方向会在更新 \(y\) 的时候被计算。

拆一下就有:

\[\text{Ans}=\max\{dp_y-pre_y\}+pre_x \]

由于 \(x,y\) 的距离需要小于 \(m\)\(m\) 是环长),所以用单调队列维护。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f|=(s=='-');
	while(s>='0' and s<='9')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-'),write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
} 

#include <deque>
#include <vector>
#include <iostream>
using namespace std;
typedef long long ll;

const int maxn=1e6+5;

int n,head[maxn],cnt,f[maxn];
ll dp[maxn],len;
int vis[maxn],Val[maxn];
struct edge {
	int nxt,to,w;
} e[maxn<<1];
vector <int> rt;
struct node {
	int id; ll d;
};
deque <node> q; 

void addEdge(int u,int v,int val) {
	e[++cnt].w=val;
	e[cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
	f[v]=u;
}

void dfs(int u) {
	if(!vis[u]) vis[u]=1;
	for(int i=head[u];i;i=e[i].nxt) {
		int v=e[i].to;
		if(vis[v]==2) continue;
		dfs(v);
		len=max(len,dp[u]+dp[v]+e[i].w);
		dp[u]=max(dp[u],dp[v]+e[i].w);
	}
}

ll work(int x) {
	rt.clear();
	while(vis[x]!=2) {
		if(vis[x])
			rt.push_back(x);
		++vis[x];
		x=f[x];
	}
	ll ret=0,s=0,tmp=0;
	for(auto i:rt) {
		len=0;
		dfs(i);
		ret=max(ret,len);
	}
	int m=rt.size();
	for(int i=0;i<m;++i)
		rt.push_back(rt[i]);
	while(!q.empty()) q.pop_back();
	for(int i=0;i<(m<<1);++i) {
		while(!q.empty() and i-q.front().id>=m)
			q.pop_front();
		if(!q.empty())
			ret=max(ret,dp[rt[i]]+q.front().d+s);
		while(!q.empty() and q.back().d<=dp[rt[i]]-s)
			q.pop_back();
		q.push_back((node){i,dp[rt[i]]-s});
		s+=Val[rt[i]];
	}
	return ret;
}

int main() {
	n=read(9);
	int y,w;
	for(int i=1;i<=n;++i) {
		y=read(9),Val[i]=w=read(9);
		addEdge(y,i,w);
	}
	ll ans=0;
	for(int i=1;i<=n;++i)
		if(!vis[i])
			ans+=work(i);
	print(ans,'\n');
	return 0;
}

并不知道如何归类

\(\text{[NOIP 2018] }\)旅行

解法

\(m=n-1\) 是很简单的。先开始从 \(1\) 开始,每次找最小的点,因为每个点只能遍历一次,而且每个点必须被遍历,所以必须遍历完子树再回去,不然之后就不可能再遍历到了。

对于 \(m=n\),有可能出现半路返回再通过环遍历到子树,我们称之为回溯,情况就有些棘手了。但是,回溯只可能在环上发生,且回溯一次相当于 \(\rm ban\) 掉一条边,之后就变成一棵树了,所以回溯只可能发生一次。

我们发现,在 \(u\) 处进行回溯时(假设 \((u,v)\) 是环上的边),必须将 \(u\) 连接的不在环上的边的子树都走一遍。所以当 \(v\)\(u\) 剩余没走的点中最大的点时,回溯才可能是更优的。另外,我们还需要保证回溯到上一层中走的第一个点小于 \(v\)

算法大概是这样的,但是题解的实现我觉得好神仙,是 \(\mathcal O(n\log n)\) 的。我在下面附了注释。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f|=(s=='-');
	while(s>='0' and s<='9')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-'),write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
} 

#include <vector>
#include <algorithm>
using namespace std;

const int maxn=5e5+5;

vector <int> ans;
int n,m,stk[maxn],tp,head[maxn];
int cnt,pre=maxn;
bool done;
bool vis[maxn],flag,on[maxn];
struct edge {
	int nxt,to;
} e[maxn<<1];
struct Edge {
	int u,v;
	
	bool operator < (const Edge &t) const {
		return v>t.v;
	}
} E[maxn<<1];

void addEdge(int u,int v) {
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
} 

void findCircle(int u,int fa) {
	stk[++tp]=u; vis[u]=1;
	for(int i=head[u];i;i=e[i].nxt) {
		int v=e[i].to;
		if(v==fa) continue;
		if(vis[v]) {
			while(stk[tp]^v)
				on[stk[tp--]]=1;
			on[v]=1;
			flag=1; break;
		}
		findCircle(v,u);
		if(flag) return;
	}
	--tp;
}

void dfs(int u) {
	vis[u]=1;
	ans.push_back(u);
	if(!on[u]) {
		for(int i=head[u];i;i=e[i].nxt)
			if(!vis[e[i].to])
				dfs(e[i].to);
		return;
	}
	bool f=0;
	for(int i=head[u];i;i=e[i].nxt) {
		if(done) break;
		int v=e[i].to;
		if(vis[v]) continue;
		if(on[v]) {
			i=e[i].nxt;
			// 特判一下父亲
			while(vis[e[i].to])
				i=e[i].nxt;
			// 当 i!=0 时,说明 v 不是当前剩余的点中最大的点,所以沿顺序继续走,但是要记录一下回溯时选择的最小的点 pre
			if(i) pre=e[i].to;
			else if(v>pre) f=done=1;
			break;
		}
	}
	for(int i=head[u];i;i=e[i].nxt) {
		int v=e[i].to;
		if(vis[v] or (on[v] and f)) continue;
		dfs(v);
	}
}

int main() {
	n=read(9),m=read(9);
	for(int i=1;i<=m;++i) {
		int u,v;
		u=read(9),v=read(9);
		E[i]=(Edge){u,v};
		E[i+m]=(Edge){v,u};
	}
	sort(E+1,E+(m<<1)+1);
	// 将边按 v 从大到小排序,这样前向星遍历时 v 就是从小到大的顺序
	for(int i=1;i<=(m<<1);++i)
		addEdge(E[i].u,E[i].v);
	findCircle(1,0);
	fill(&vis[1],&vis[n]+1,0);
	dfs(1);
	for(auto i:ans) print(i,' ');
	puts("");
	return 0;
}
posted on 2021-09-06 16:12  Oxide  阅读(51)  评论(0编辑  收藏  举报