题目
点这里看题目。
分析
玄学题目。直接搬运题解.jpg
解决这道题首先需要用到处理强连通图的一种特殊方法:耳分解
定理:一个有向图 (G=(V,E)) 为强连通,当且仅当它可以通过如下方法构造:
维护一个图 (G'=(V',E'));初始时,(V'={v},E'=varnothing),其中 (vin V);
循环地执行接下来两步,直到 (G'=G) 或无法执行:
选取 (x,yin V'),(x) 可以等于 (y);
选取零个或多个互不相同的点 (u_1,u_2,dots,u_k),满足 (forall 1le ile k,u_i otin V'),同时还满足 ((x,u_1)in E,(u_k,y)in E,forall1le i<k,(u_i,u_{i+1})in E)。
将路径 (x ightarrow u_1 ightarrow u_2 ightarrow dots ightarrow u_k ightarrow y) 加入到 (G') 中。
这里的路径 (x ightarrow u_1 ightarrow u_2 ightarrow dots ightarrow u_k ightarrow 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 ightarrow u_1 ightarrow u_2 ightarrow dots ightarrow u) 的路径、耳的终点在 (v)、当前不可以/可以连接 ((u,v)) 的情况下,最小的花费。转移可以枚举 (u) 的下一个点 (w),转移到 (g_{Scup{w},w,v,1});如果可以连接 ((u,v)),也可以直接转移到 (f_S)。此时 (f) 也可以参与转移,同样是枚举新耳在 (S) 内的起点 (x) 和终点 (y);由于此时只有一条边的耳不需要考虑,我们可以直接枚举下一个点 (w,w otin 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( '
' );
return 0;
}