[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)\) 的算法,细节也比较少。

小结:

  1. 关注一下“耳分解”这种处理强连通图的方法;
  2. 如果多种方案有不同代价,从中必选一个,那么可以先选择最优的再来调整方案,记录一下这种方法;

代码

#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;
}
posted @ 2021-08-15 14:25  crashed  阅读(210)  评论(0编辑  收藏  举报