「CF1710D」Recover the Tree
题目
给定一个 \(n\times n\) 的 01 矩阵(的上三角部分)\(A_{n\times n}\)。
构造一棵有 \(n\) 个结点的树,满足:
对于任意的 \(1\le l\le r\le n\),编号在 \([l,r]\) 内的结点在树上构成一个连通块当且仅当 \(A_{l,r}=1\)。
数据保证有解。
单个数据点内有多组测试数据。所有数据点满足 \(1\le n\le 2000, \sum n\le 2000\)。
分析
没什么用的想法:如果图 \(G\) 是森林,那么 \(|V|\ge|E|+1\),且当且仅当 \(|V|=|E|+1\) 时 \(G\) 是树。
这个性质最后并没能帮助我们构造,但是它却很好地帮助我们解决了“生成数据”和“检查输出”的问题。😄
考虑极小的情况,如果存在 \(A_{u,u+1}=1\),则我们就直接找到了一条边。那么,我们可以先把这条边连起来。之后的构造中,我们几乎可以直接将 \(u,u+1\) 看作一个结点处理,也即缩点。
考虑极大的情况,在这里就是 \(A\) 形如:
此时构造方法比较多样,过程也比较简单。这里举出我的一例:
连接 \((n,1)\) 和 \((n,2)\)。对于 \(3\le k<n\),连接 \((k,k-2)\)。
那么,怎么把上面的两个观察凑成一个算法?
找一条显在的边其实可以扩展为找一个极小的连通块。也就是一个区间 \([l,r]\),满足任意的 \(l\le l'<r'\le r\) 且 \((l,r)\neq (l',r')\) 的区间 \([l',r']\) 都不连通。我们希望可以直接将它按照“极大”情况构造,然后收缩。很明显,不断的收缩我们最后一定可以得到一棵树。
但是,如果极小的连通块之间“非平凡”有交,那似乎会破坏我们的性质。也即,如果有 \(l_1\le l_2<r_1\le r_2\),且 \(A_{l_1,r_1}=1,A_{l_2,r_2}=1\),那么我们的“收缩”似乎就是不成立的,因为最终相交这一块很难处理。
但是我们再联系一下背景,这是一棵树。画图感悟一下可以发现,此时 \([l_2,r_1]\) 也应该是连通块,而想到这一点之后证明就很容易了。因此,极小连通块之间的交最多就是平凡的单点,而这对于构造的影响就很小了。
到了这一步,我们可以得到一个朴素的算法框架:反复地寻找全局极小的区间,构造,然后收缩矩阵 \(A\),直到剩余结点数量为 \(1\)。
这个算法中有几个问题:
-
反复迭代,复杂度可能会退化成 \(O(n^3)\)。
不过,我们没有必要显式地对于矩阵进行收缩,甚至这个“反复迭代”的过程也是不必要的。
这就好比朱刘算法的 Tarjan 优化:区间之间的处理顺序并不是很严格,所以完全可以增量地加入点,并且将增量的区间处理掉。
-
增量合并时,一个需要合并的区间可能只包含了某个连通块的一部分。相应地,我们只能向这一部分中的点连边。
-
在合并时,一个“单点”代表的其实是一个区间 \([l',r']\),那么后续连边可以连接到区间中的任意一个点,但是有的是不合法的!
比如第一组样例中,处理完 \(1\sim 3\) 后再加入 \(4\),此时我们需要打通 \([2,3]\) 和 \(4\),但连接 \((3,4)\) 就是不合法的。
注意到,在这样的条件下,合并 \([l,r]\) 时它是极小的,则我们应当尽量避免出现更小的连通块。上述例子中我们就应该 \((2,4)\),再拓展一下就应该是靠前区间的编号最小结点连向靠后区间的编号最大结点。
最后就可以 \(O(n^2)\) 地解决了。
代码
#include <cstdio>
#include <utility>
#include <algorithm>
#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 MAXN = 2005;
template<typename _T>
inline void Read( _T &x ) {
x = 0; char s = getchar(); bool f = false;
while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
if( f ) x = -x;
}
template<typename _T>
inline void Write( _T x ) {
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) Write( x / 10 );
putchar( x % 10 + '0' );
}
typedef std :: pair<int, int> Edge;
Edge edg[MAXN]; int len;
int mat[MAXN][MAXN];
int mn[MAXN], mx[MAXN];
int seq[MAXN], tot;
int stk[MAXN], top;
int N;
inline void Link( const int &u, const int &v ) {
edg[++ len] = Edge( u, v );
}
int Merge( const int &l, const int &r ) {
if( tot == 2 )
Link( std :: max( mn[seq[1]], l ),
std :: min( mx[seq[2]], r ) );
else if( tot > 2 ) {
per( i, tot - 1, 3 )
Link( mx[seq[i]], mx[seq[i - 2]] );
Link( std :: max( mn[seq[1]], l ), mx[seq[tot]] );
Link( std :: max( mn[seq[2]], l ), mx[seq[tot]] );
}
mn[seq[tot]] = mn[seq[1]];
return seq[tot];
}
int main() {
int T; Read( T );
while( T -- ) {
Read( N ), top = len = 0;
rep( i, 1, N ) rep( j, i, N )
scanf( "%1d", &mat[i][j] );
rep( i, 1, N ) mn[i] = mx[i] = i;
rep( i, 1, N ) {
stk[++ top] = i;
per( j, i - 1, 1 )
if( mat[j][i] ) {
// 合并 [j, i] 范围内的点
// 栈里面放的是每个连通块的并查集的根
tot = 0;
while( top > 0 && j <= stk[top] )
seq[++ tot] = stk[top --];
std :: reverse( seq + 1, seq + 1 + tot );
stk[++ top] = Merge( j, i );
}
}
rep( i, 1, N - 1 )
Write( edg[i].first ), putchar( ' ' ),
Write( edg[i].second ), putchar( '\n' );
}
return 0;
}