zoukankan      html  css  js  c++  java
  • [题目小结] 基环树专练

    破环为树

    ( ext{[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,'
    ');
    	return 0;
    }
    

    ( ext{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
    ",minval,plans);
    	}
    	return 0;
    }
    

    在环上合并

    ( ext{Island })岛屿

    解法

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

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

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

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

    拆一下就有:

    [ ext{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,'
    ');
    	return 0;
    }
    

    并不知道如何归类

    ( ext{[NOIP 2018] })旅行

    解法

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

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

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

    算法大概是这样的,但是题解的实现我觉得好神仙,是 (mathcal O(nlog 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;
    }
    
  • 相关阅读:
    贷款计算公式
    P2P行业专业术语(最全)
    p2p投资理财入门篇(新手必备)
    2015年p2p网络借贷平台的发展现状
    MyEclipse中SVN的常见的使用方法
    linux下打开、关闭tomcat,实时查看tomcat运行日志
    Spring 向页面传值以及接受页面传过来的参数的方式
    Spring自定义一个拦截器类SomeInterceptor,实现HandlerInterceptor接口及其方法的实例
    PowerDesigner概述(系统分析与建模)以及如何用PowerDesigner快速的创建出这个数据库
    MySQL 8.x 函数和操作符,官方网址:https://dev.mysql.com/doc/refman/8.0/en/functions.html
  • 原文地址:https://www.cnblogs.com/AWhiteWall/p/15234242.html
Copyright © 2011-2022 走看看