破环为树
( 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) 可以这样更新:
本来需要考虑 (x) 到 (y) 在环上有两条路径,但由于破环为链,另一个方向会在更新 (y) 的时候被计算。
拆一下就有:
由于 (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;
}