路径覆盖是一个路径的集合使得每个顶点都只被一条路径覆盖。最小路径覆盖问题要求集合中路径的条数是最小的。
树的最小路径覆盖 #
做法 1:DP #
d p u , 0 dp_{u, 0} d p u , 0 代表当 u 不为路径的端点的时候,u 的子树里最少的路径的数目,d p u , 1 dp_{u, 1} d p u , 1 代表当 u 为路径的端点的时候,u 的子树里最少的路径的数目。
设v v v 为 u 的儿子,状态转移时 u 不为端点的情况可以是之前 u 不为端点的情况加上 v 不为端点的情况,即:
d p u , 0 ≔ d p u , 0 + d p v , 0 dp_{u, 0}\coloneqq dp_{u, 0}+dp_{v, 0} d p u , 0 : = d p u , 0 + d p v , 0
也可以是以 u 为端点的路与以 v 为端点 的路连成一条路,即:
d p u , 0 ≔ d p u , 1 + d p v , 1 − 1 dp_{u, 0}\coloneqq dp_{u, 1}+dp_{v, 1}-1 d p u , 0 : = d p u , 1 + d p v , 1 − 1
u 为端点的情况类似,可以是之前 u 为端点的情况加上 v 不为端点的情况,即:
d p u , 1 ≔ d p u , 1 + d p v , 0 dp_{u, 1}\coloneqq dp_{u, 1}+dp_{v, 0} d p u , 1 : = d p u , 1 + d p v , 0
也可以是前面所有儿子的不以儿子为端点的路径加上以 v 为端点的路径,即:
d p u , 1 ≔ s u m + d p v , 1 dp_{u, 1}\coloneqq sum+dp_{v, 1} d p u , 1 : = s u m + d p v , 1
综上所述:
d p u , 0 ≔ min ( d p u , 0 + d p v , 0 , d p u , 1 + d p v , 1 − 1 ) d p u , 1 ≔ min ( d p u , 1 + d p v , 0 , s u m + d p v , 1 ) \begin{align*} dp_{u, 0}&\coloneqq \min(dp_{u, 0}+dp_{v, 0}, dp_{u, 1}+dp_{v, 1}-1)\\\ dp_{u, 1}&\coloneqq \min(dp_{u, 1}+dp_{v, 0}, sum+dp_{v, 1})\end{align*} d p u , 0 d p u , 1 : = min ( d p u , 0 + d p v , 0 , d p u , 1 + d p v , 1 − 1 ) : = min ( d p u , 1 + d p v , 0 , s u m + d p v , 1 )
如果要记录方案的话只先在 dp 的过程中记录经过 u 的路径往下走的儿子,然后再跑一遍 dfs 构建路径。
代码:
vector dp ( n , vector < int >( 2 )) ;
vector nxt ( n , vector ( 2 , pair{ - 1 , - 1 } )) ;
auto dfs =[ & ] ( auto & dfs , int u , int p ) -> void {
dp [ u ][ 0 ] = dp [ u ][ 1 ] = 1 ;
int sum = 0 ;
for ( auto v : g [ u ]) {
if (v == p) continue ;
dfs (dfs , v , u) ;
if ( dp [u][ 0 ] + dp [v][ 0 ] > dp [u][ 1 ] + dp [v][ 1 ] - 1 ) {
nxt [u][ 0 ] = { nxt [u][ 1 ] . first , v} ;
}
dp [u][ 0 ] = min ( dp [u][ 0 ] + dp [v][ 0 ] , dp [u][ 1 ] + dp [v][ 1 ] - 1 ) ;
if ( dp [u][ 1 ] + dp [v][ 0 ] > sum + dp [v][ 1 ]) {
nxt [u][ 1 ] = {v , v} ;
}
dp [u][ 1 ] = min ( dp [u][ 1 ] + dp [v][ 0 ] , sum + dp [v][ 1 ]) ;
sum += dp [v][ 0 ] ;
}
} ;
vector < vector < int >> end_point ( n ) ; //路径的端点
vector < pii > remove ; // 不在路径覆盖中的路径
int tot {} ;
auto dfs2 =[ & ] ( auto & dfs2 , int u , int p , int flag , int id ) -> void { // id 为当前路径的编号
for ( auto v : g [ u ]) {
if (v == p) continue ;
if (v == nxt [u][flag] . first || v == nxt [u][flag] . second ) {
dfs2 (dfs2 , v , u , 1 , id) ;
} else {
remove . emplace_back (u , v) ;
tot ++;
int nflag = dp [v][ 0 ] < dp [v][ 1 ] ? 0 : 1 ;
if (nflag) end_point [tot] . push_back (v) ;
dfs2 (dfs2 , v , u , nflag , tot) ;
}
}
if ( nxt [ u ][ flag ] == pair{ - 1 , - 1 } ) end_point [ id ] . push_back ( u ) ;
} ;
做法 2:贪心 #
贪心做法更加简单,只用一个 dfs 就能实现。如果 u 有两个儿子是路径的端点那么就连接那两条路,否则就将 u 做为端点。
代码:
vector < pii > end_points , remove ;
auto dfs =[ & ] ( auto & dfs , int u , int p ) -> int { // 返回 -1 代表 u 不是端点,否则返回以 u 为端点的路径的另一端。
vector <int> next ;
for ( auto v : g [ u ]) {
if (v == p) continue ;
int end_v = dfs (dfs , v , u) ;
if (end_v >= 0 ) {
if ( next . size () <= 1 ) {
next . push_back (end_v) ;
} else {
remove . emplace_back (u , v) ;
end_points . emplace_back (end_v , v) ;
}
} else {
remove . emplace_back (u , v) ;
}
}
if ( next . empty ()) next . push_back ( u ) ;
if ( next . size () == 1 ) {
if (p != - 1 ) return next [ 0 ] ;
end_points . emplace_back ( next [ 0 ] , u) ;
return - 1 ;
} else {
end_points . emplace_back ( next [ 0 ] , next [ 1 ]) ;
return - 1 ;
}
} ;
练习题 #
CF1521D - Nastia Plays with a Tree
DAG 的最小路径覆盖 #
我们把原图上的每个点拆成两个点(对于点x
,可以把从它拆出去的点记为x+n
),其中一个点与源点相连,另一个与汇点相连。对于原 DAG 上的边u -> v
,在新图中连接 u -> v'
,所有边的容量均为 1。跑一遍最大流(本质上是二分图匹配),得到的最大流(或者最大匹配)便是被覆盖的边数,由于路径上的点数等于边数 +1,所以点数减被覆盖的边数便是路径的数目。也可以理解为最大流经过的每一条边对应原图中有一条向边的起点,所以路径的终点是没有对应的边的,所以点数减被覆盖的边数便是终点的数目也就是路径的数目。
如何记录路径?可以在增广途中记录每个点的下一个点。如何找起点?如果x'
到汇点的剩余容量为 1,说明没有点流向x
,也就说明x
是起点。
模板题:
洛谷 P2764 最小路径覆盖问题
代码:
# include < bits/stdc++.h >
using namespace std ;
struct Flow {
static constexpr int INF = 1 e 9 ;
int n ;
struct Edge {
int to , cap ;
Edge ( int to , int cap ) : to (to) , cap (cap) {}
} ;
std :: vector < Edge > e ;
std :: vector < std :: vector <int >> g ;
std :: vector <int> cur , h , nxt ;
Flow ( int n ) : n (n) , g (n) , nxt (n) {}
bool bfs ( int s , int t ) {
h . assign (n , - 1 ) ;
std :: queue <int> que ;
h [s] = 0 ;
que . push (s) ;
while ( ! que . empty ()) {
int u = que . front () ;
que . pop () ;
for ( int i : g [u]) {
auto [v , c] = e [i] ;
if (c > 0 && h [v] == - 1 ) {
h [v] = h [u] + 1 ;
if (v == t) return true ;
que . push (v) ;
}
}
}
return false ;
}
int dfs ( int u , int t , int f ) {
if (u == t) return f ;
int r = f ;
for ( int & i = cur [u] ; i < int ( g [u] . size ()) ; ++ i) {
int j = g [u][i] ;
auto [v , c] = e [j] ;
if (c > 0 && h [v] == h [u] + 1 ) {
int a = dfs (v , t , std :: min (r , c)) ;
e [j] . cap -= a ;
e [j ^ 1 ] . cap += a ;
r -= a ;
if (a) nxt [u] = v ; // 增广成功便记录路径
if (r == 0 ) return f ;
}
}
return f - r ;
}
void addEdge ( int u , int v , int c ) {
g [u] . push_back (( int ) e . size ()) ;
e . emplace_back (v , c) ;
g [v] . push_back (( int ) e . size ()) ;
e . emplace_back (u , 0 ) ;
}
void maxFlow ( int s , int t ) {
int ans = 0 ;
while ( bfs (s , t)) {
cur . assign (n , 0 ) ;
ans += dfs (s , t , INF) ;
}
n = (n - 2 ) / 2 ;
for ( int i = n + 1 ; i <= 2 * n ; i ++ ) {
if ( e [ g [i] . back ()] . cap == 1 ) {
int u = i - n ;
while (u > 0 ) {
cout << u << ' ' ;
u = nxt [u] - n ;
}
cout << '\n' ;
}
}
cout << n - ans << '\n' ;
}
} ;
int main () {
ios :: sync_with_stdio ( false ) ;
cin . tie ( nullptr ) ;
int n , m ;
cin >> n >> m ;
Flow g ( 2 * n + 2 ) ;
while ( m -- ) {
int u , v ;
cin >> u >> v ;
g . addEdge (u , v + n , 1 ) ;
}
for ( int i = 1 ; i <= n ; i ++ ) {
g . addEdge ( 0 , i , 1 ) ;
g . addEdge (i + n , 2 * n + 1 , 1 ) ;
}
g . maxFlow ( 0 , 2 * n + 1 ) ;
return 0 ;
}
参考资料 #
https://zhuanlan.zhihu.com/p/125759333