[Gym102759C] Economic One-way Roads
题目
点这里看题目。
分析
玄学题目。直接搬运题解.jpg
解决这道题首先需要用到处理强连通图的一种特殊方法:耳分解
定理:一个有向图 \(G=(V,E)\) 为强连通,当且仅当它可以通过如下方法构造:
维护一个图 \(G'=(V',E')\);初始时,\(V'=\{v\},E'=\varnothing\),其中 \(v\in V\);
循环地执行接下来两步,直到 \(G'=G\) 或无法执行:
选取 \(x,y\in V'\),\(x\) 可以等于 \(y\);
选取零个或多个互不相同的点 \(u_1,u_2,\dots,u_k\),满足 \(\forall 1\le i\le k,u_i\not\in V'\),同时还满足 \((x,u_1)\in E,(u_k,y)\in E,\forall1\le i<k,(u_i,u_{i+1})\in E\)。
将路径 \(x\rightarrow u_1\rightarrow u_2\rightarrow \dots\rightarrow u_k\rightarrow y\) 加入到 \(G'\) 中。
这里的路径 \(x\rightarrow u_1\rightarrow u_2\rightarrow \dots\rightarrow u_k\rightarrow y\) 就是“耳”。
容易使用归纳法证明,这样构造出来的图一定是强连通的。
但是充分性我还是不会证明
根据耳分解,我们不难想到根据这个过程进行 DP。设 \(f_S\) 表示当前 \(V'=S\),所需的最小花费。转移可以枚举一个“耳”,对于 \(S\) 总共有不超过 \(2^{n-|S|}S^2\) 种需要枚举检验的耳的方案。这样复杂度是 \(O(3^nn^2)\) 的。
但是转移会出现两个问题:其一,有可能有些边压根没有被计算到;其二,由于耳可以只有一条边,所以 \(f_S\) 会出现环形转移。
一种巧妙的解决方法是:对于原图上的某条边,它的两种定向方案的花费分别为 \(a,b\)。由于我们一定要对它定向,那么我们可以先钦定它取花费较小的方向,也就是 \(\min\{a,b\}\) 对应的方向;这样再定向的花费就变成了 \(a-\min\{a,b\},b-\min\{a,b\}\)。
这样我们就得到了正确但超时的算法。但是注意到在这个算法中,很多种耳的方案都是不合法的。我们可以通过先选取 \(x,y\),再在 \(E\) 上遍历出一条合法的耳。事实上,我们可以直接将当前的遍历信息记录到状态中:
设新状态 \(g_{S,u,v,0/1}\) 表示当前 \(V'\) 和一个未完成的耳的并集为 \(S\)、当前已经完成了 \(x\rightarrow u_1\rightarrow u_2\rightarrow \dots\rightarrow u\) 的路径、耳的终点在 \(v\)、当前不可以/可以连接 \((u,v)\) 的情况下,最小的花费。转移可以枚举 \(u\) 的下一个点 \(w\),转移到 \(g_{S\cup\{w\},w,v,1}\);如果可以连接 \((u,v)\),也可以直接转移到 \(f_S\)。此时 \(f\) 也可以参与转移,同样是枚举新耳在 \(S\) 内的起点 \(x\) 和终点 \(y\);由于此时只有一条边的耳不需要考虑,我们可以直接枚举下一个点 \(w,w\not\in S\) 进行转移。注意,如果 \(x=y\),那么现在就不能连接 \((w,y)\)——我们不能将一条边定向两次。
这样我们得到了一个复杂度为 \(O(2^nn^3)\) 的算法,细节也比较少。
小结:
- 关注一下“耳分解”这种处理强连通图的方法;
- 如果多种方案有不同代价,从中必选一个,那么可以先选择最优的再来调整方案,记录一下这种方法;
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )
const int INF = 0x3f3f3f3f;
const int MAXN = 18, MAXS = 1 << 18;
template<typename _T>
void read( _T &x )/*{{{*/
{
x = 0; char s = getchar(); int f = 1;
while( ! ( '0' <= s && s <= '9' ) ) { f = 1; if( s == '-' ) f = -1; s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
x *= f;
}/*}}}*/
template<typename _T>
void write( _T x )/*{{{*/
{
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) write( x / 10 );
putchar( x % 10 + '0' );
}/*}}}*/
int dp[MAXS][MAXN][MAXN][2];
int f[MAXS];
int G[MAXN][MAXN];
int N;
void Upt( int &x, const int v ) { x = std :: min( x, v ); }
int main()
{
read( N ); int ans = 0, lim;
for( int i = 0 ; i < N ; i ++ )
for( int j = 0 ; j < N ; j ++ )
read( G[i][j] );
for( int i = 0 ; i < N ; i ++ )
for( int j = i + 1 ; j < N ; j ++ )
{
if( G[i][j] < 0 ) continue;
int t = std :: min( G[i][j], G[j][i] );
ans += t, G[i][j] -= t, G[j][i] -= t;
lim += t + G[i][j] + G[j][i];
}
int all = ( 1 << N ) - 1;
memset( f, 0x3f, sizeof f );
memset( dp, 0x3f, sizeof dp );
f[1] = 0;
for( int S = 1 ; S <= all ; S ++ )
{
for( int u = 0 ; u < N ; u ++ )
for( int v = 0 ; v < N ; v ++ )
for( int b = 0 ; b <= 1 ; b ++ )
{
if( dp[S][u][v][b] > lim ) continue;
for( int T = all ^ S ; T ; T &= T - 1 )
{
int w = __builtin_ctz( T );
if( G[u][w] >= 0 )
Upt( dp[S | ( 1 << w )][w][v][1], dp[S][u][v][b] + G[u][w] );
}
if( b && G[u][v] >= 0 )
Upt( f[S], dp[S][u][v][b] + G[u][v] );
}
for( int u = 0 ; u < N ; u ++ ) if( S >> u & 1 )
for( int v = 0 ; v < N ; v ++ ) if( S >> v & 1 )
for( int w = 0 ; w < N ; w ++ ) if( ! ( S >> w & 1 ) )
{
if( G[u][w] < 0 ) continue;
Upt( dp[S | ( 1 << w )][w][v][u != v], f[S] + G[u][w] );
}
}
if( f[all] > lim ) puts( "-1" );
else write( ans + f[all] ), putchar( '\n' );
return 0;
}