YbtOJ 「图论」 第5章 强连通分量
强连通分量 金牌导航
A. 【例题1】受欢迎的牛
缩点后判断每一个强连通分量的出度 没有出度的就是明星 如果有两个或以上没有出度的 直接输出0即可
注意强连通分量缩点退栈的时候scc大小需要自增维护
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , f[N] , ans , a[N] , x[N] , y[N] , temp = 0 , cd[N];
int head[N] , cnt;
struct node { int to , nxt; } e[N];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
int dfn[N] , low[N] , timer;
int scc[N] , id[N] , tot;
int in[N] , sta[100000000] , tp;
void tarjan ( int u )
{
dfn[u] = low[u] = ++timer;
sta[++tp] = u , in[u] = 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( !dfn[v] ) tarjan(v) , low[u] = min ( low[u] , low[v] );
else if ( in[v] ) low[u] = min ( low[u] , dfn[v] );
}
if ( dfn[u] == low[u] )
{
tot ++;
while ( tp )
{
int x = sta[tp--];
id[x] = tot , in[x] = 0 , scc[tot] ++;
if ( x == u ) break;
}
}
}
signed main ()
{
n = read() , m = read();
for ( int i = 1 ; i <= m ; i ++ ) x[i] = read() , y[i] = read() , add ( x[i] , y[i] );
for ( int i = 1 ; i <= n ; i ++ ) if ( !dfn[i] ) tarjan(i);
for ( int i = 1 ; i <= m ; i ++ ) if ( id[x[i]] != id[y[i]] ) cd[id[x[i]]] ++;
for ( int i = 1 ; i <= tot ; i ++ )
if ( !cd[i] )
{
if ( temp ) { cout << "0" << endl; return 0; }
temp = i;
}
cout << scc[temp] << endl;
return 0;
}
B. 【例题2】最大半连通子图
[题目描述]
一个有向图 \(G=\left(V,E\right)\) 称为半连通的 (Semi-Connected),如果满足:\(\forall u,v\in V\),满足 \(u\to v\) 或 \(v\to u\),即对于图中任意两点 \(u,v\),存在一条 \(u\) 到 \(v\) 的有向路径或者从 \(v\) 到 \(u\) 的有向路径。
若 \(G'=\left(V',E'\right)\) 满足 \(V'\subseteq V\),\(E'\) 是 \(E\) 中所有跟 \(V'\) 有关的边,则称 \(G'\) 是 \(G\) 的一个导出子图。若 \(G'\) 是 \(G\) 的导出子图,且 \(G'\) 半连通,则称 \(G'\) 为 \(G\) 的半连通子图。若 \(G'\) 是 \(G\) 所有半连通子图中包含节点数最多的,则称 \(G'\) 是 \(G\) 的最大半连通子图。
给定一个有向图 \(G\),请求出 \(G\) 的最大半连通子图拥有的节点数 \(K\),以及不同的最大半连通子图的数目 \(C\)。由于 \(C\) 可能比较大,仅要求输出 \(C\) 对 \(X\) 的余数。
[输入格式]
第一行包含两个整数 \(N,M,X\)。\(N,M\)分别表示图 \(G\) 的点数与边数,\(X\) 的意义如上文所述。
接下来 \(M\) 行,每行两个正整数 \(a,b\),表示一条有向边 \(\left(a,b\right)\)。图中的每个点将编号为 \(1,2,3\dots N\),保证输入中同一个\(\left(a,b\right)\)不会出现两次。
[输出格式]
应包含两行,第一行包含一个整数 \(K\),第二行包含整数 \(C\bmod X\)。
[算法分析]
首先缩点 scc必定是半连通子图
而且在这个有向无环图(DAG)中,半连通子图都是一条链(可以举反例试试,这条链不可能有分支,否则将有两点无法抵达另一方 如果有分支且能到达另一方的 这个点显然会在缩点的时候被缩掉)
所以问题就转化为了在这个dag中找最长链及最长链的个数
注意需要去重(可以利用used数组记录上一次这个点的出边 如果重复就跳过 而且因为scc缩点都是在一起的 所以重边也在一起 可以用这种方法去重)
记录f[i]以i为结尾的最长链大小 g[i]是与这个子图大小f[i]相等的子图的数目 即可dp
[代码实现]
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;
inline int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , x[N] , y[N] , mod , f[N] , ans[N] , used[N];
//f[i]以i为结尾的最长链大小 g[i]是与这个子图大小f[i]相等的子图的数目 used表示这个点上一次是从哪儿转移过来的 为了判断重边
int head[N] , cnt = 1;
struct node { int to , nxt , w; } e[20000005];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
int sta[N] , top;//手写栈
int low[N] , dfn[N] , in[N] , timer;
int tot , scc[N] , id[N];//tot是强连通分量个数 scc是对应强连通分量的权值 id是原图中的点属于哪一个强连通分量
void tarjan ( int u )
{
dfn[u] = low[u] = ++timer;
sta[++top] = u; in[u] = 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( !dfn[v] ) tarjan(v) , low[u] = min ( low[u] , low[v] );
else if ( in[v] ) low[u] = min ( low[u] , dfn[v] );
}
if ( low[u] == dfn[u] )
{
tot ++;
while ( top != -1 )
{
int x = sta[top--];
id[x] = tot;
in[x] = 0;
scc[tot] ++;
if ( x == u ) break;
}
}
}
void init()
{
memset ( head , 0 , sizeof ( head ) );
cnt = 1;
}
signed main ()
{
n = read() , m = read() , mod = read();
for ( int i = 1 ; i <= m ; i ++ ) x[i] = read() , y[i] = read() , add ( x[i] , y[i] );
for ( int i = 1 ; i <= n ; i ++ ) if ( !dfn[i] ) tarjan(i);
init();
for ( int i = 1 ; i <= m ; i ++ )
if ( id[x[i]] != id[y[i]] ) add ( id[x[i]] , id[y[i]] );
for ( int i = 1 ; i <= n ; i ++ ) f[i] = scc[i] , ans[i] = 1;
for ( int u = tot ; u ; u -- )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( used[v] == u ) continue;//如果上一条边也是这个那么跳过 因为scc统计都是在一起的 所以重边也排列在一起
used[v] = u;
if ( f[v] < f[u] + scc[v] )
{
f[v] = f[u] + scc[v];
ans[v] = ans[u];//所有与u子图相等的方案数都可以加一变成这个
}
else if ( f[v] == f[u] + scc[v] )
{
ans[v] += ans[u];//这个大小和原来的大小一样 图的数量累加
ans[v] %= mod;
}
}
}
int maxsize = 0 , sum = 0;
for ( int i = 1 ; i <= n ; i ++ )
{
if ( f[i] > maxsize )
{
maxsize = f[i];
sum = ans[i];
}
else if ( f[i] == maxsize )
{
sum += ans[i];
sum %= mod;
}
}
printf ( "%d\n%d" , maxsize , sum );
return 0;
}
C. 网络协议
[题目描述]
一些学校连入一个电脑网络。那些学校已订立了协议:每个学校都会给其它的一些学校分发软件(称作“接受学校”)。注意即使 \(B\) 在 \(A\) 学校的分发列表中,\(A\) 也不一定在 \(B\) 学校的列表中。
你要写一个程序计算,根据协议,为了让网络中所有的学校都用上新软件,必须接受新软件副本的最少学校数目(子任务 A)。更进一步,我们想要确定通过给任意一个学校发送新软件,这个软件就会分发到网络中的所有学校。为了完成这个任务,我们可能必须扩展接收学校列表,使其加入新成员。计算最少需要增加几个扩展,使得不论我们给哪个学校发送新软件,它都会到达其余所有的学校(子任务 B)。一个扩展就是在一个学校的接收学校列表中引入一个新成员。
[输入格式]
输入文件的第一行包括一个正整数 \(N\),表示网络中的学校数目。学校用前 \(N\) 个正整数标识。
接下来 \(N\) 行中每行都表示一个接收学校列表(分发列表),第 \(i+1\) 行包括学校 \(i\) 的接收学校的标识符。每个列表用 \(0\) 结束,空列表只用一个 \(0\) 表示。
[输出格式]
你的程序应该在输出文件中输出两行。
第一行应该包括一个正整数,表示子任务 A 的解。
第二行应该包括一个非负整数,表示子任务 B 的解。
[算法分析]
先对整张图进行缩点 缩点后能直接访问到的学校就绑定在一起了
第一问:对于每一个缩点后的节点 如果它的入度为0 那么说明软件不能传入 答案++
第二问:对于每一个缩点后的节点,缩点后,对每个出度为0的点,需拓展一条出边,
对每个入度为0的点,需拓展一条入边,则可以完成条件。
所以为了拓展的边更少,可以从出度为0的点向入度为0的点拓展边。
所以所需最少的拓展数即出度为0的点数和入度为0的点数的较大者。
[代码实现]
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;
inline int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , cd[N] , rd[N] , a , b;
int head[N] , cnt = 1;
struct node { int to , nxt; } e[N];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
int sta[N] , top;//手写栈
int low[N] , dfn[N] , in[N] , timer;
int tot , scc[N] , id[N];//tot是强连通分量个数 scc是对应强连通分量的权值 id是原图中的点属于哪一个强连通分量
void tarjan ( int u )
{
dfn[u] = low[u] = ++timer;
sta[++top] = u; in[u] = 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( !dfn[v] ) tarjan(v) , low[u] = min ( low[u] , low[v] );
else if ( in[v] ) low[u] = min ( low[u] , dfn[v] );
}
if ( low[u] == dfn[u] )
{
tot ++;
while ( top )
{
int x = sta[top--];
id[x] = tot;
in[x] = 0;
scc[tot] ++;
if ( x == u ) break;
}
}
}
signed main ()
{
n = read();
for ( int i = 1 , tmp ; i <= n ; i ++ )
{
while ( 1 )
{
tmp = read();
if ( !tmp ) break;
add ( i , tmp );
}
}
for ( int i = 1 ; i <= n ; i ++ ) if ( !dfn[i] ) tarjan ( i );
for ( int u = 1 ; u <= n ; u ++ )
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( id[u] != id[v] ) rd[id[v]] ++ , cd[id[u]] ++;
}
for ( int i = 1 ; i <= tot ; i ++ )
{
if ( !rd[i] ) a ++;
if ( !cd[i] ) b ++;
}
printf ( "%d\n" , a );
if ( tot == 1 ) printf ( "0" );
else printf ( "%d" , max ( a , b ) );
return 0;
}
D. 抢掠计划
[题目描述]
Siruseri 城中的道路都是单向的。不同的道路由路口连接。按照法律的规定,在每个路口都设立了一个 Siruseri 银行的 ATM 取款机。令人奇怪的是,Siruseri 的酒吧也都设在路口,虽然并不是每个路口都设有酒吧。
Banditji 计划实施 Siruseri 有史以来最惊天动地的 ATM 抢劫。他将从市中心出发,沿着单向道路行驶,抢劫所有他途径的 ATM 机,最终他将在一个酒吧庆祝他的胜利。
使用高超的黑客技术,他获知了每个 ATM 机中可以掠取的现金数额。他希望你帮助他计算从市中心出发最后到达某个酒吧时最多能抢劫的现金总数。他可以经过同一路口或道路任意多次。但只要他抢劫过某个 ATM 机后,该 ATM 机里面就不会再有钱了。 例如,假设该城中有 \(6\) 个路口,道路的连接情况如下图所示:
市中心在路口 \(1\),由一个入口符号 → 来标识,那些有酒吧的路口用双圈来表示。每个 ATM 机中可取的钱数标在了路口的上方。在这个例子中,Banditji 能抢劫的现金总数为 \(47\),实施的抢劫路线是:\(1-2-4-1-2-3-5\)。
[输入格式]
第一行包含两个整数 \(N,M\)。\(N\) 表示路口的个数,\(M\) 表示道路条数。
接下来 \(M\) 行,每行两个整数,这两个整数都在 \(1\) 到 \(N\) 之间,第 \(i+1\) 行的两个整数表示第 \(i\) 条道路的起点和终点的路口编号。
接下来 \(N\) 行,每行一个整数,按顺序表示每个路口处的 ATM 机中的钱数 \(a_i\)。
接下来一行包含两个整数 \(S,P\),\(S\) 表示市中心的编号,也就是出发的路口。\(P\) 表示酒吧数目。
接下来的一行中有 \(P\) 个整数,表示 \(P\) 个有酒吧的路口的编号。
[输出格式]
输出一个整数,表示 Banditji 从市中心开始到某个酒吧结束所能抢劫的最多的现金总数。
[算法分析]
将能互相到达的ATM缩点 在新图上求最短路 s相当于起点 所有的酒吧相当于终点 求这些终点的距离的最大值
[代码实现]
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
const int inf = 0x3f3f3f3f;
inline int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , ans , dis[N] , x[N] , y[N] , a[N];
int head[N] , cnt = 1;
struct node { int to , nxt , w; } e[20000005];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
int sta[N] , top;//手写栈
int low[N] , dfn[N] , in[N] , timer;
int tot , scc[N] , id[N];//tot是强连通分量个数 scc是对应强连通分量的权值 id是原图中的点属于哪一个强连通分量
void tarjan ( int u )
{
dfn[u] = low[u] = ++timer;
sta[++top] = u; in[u] = 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( !dfn[v] ) tarjan(v) , low[u] = min ( low[u] , low[v] );
else if ( in[v] ) low[u] = min ( low[u] , dfn[v] );
}
if ( low[u] == dfn[u] )
{
tot ++;
while ( top != -1 )
{
int x = sta[top--];
id[x] = tot;
in[x] = 0;
scc[tot] += a[x];
if ( x == u ) break;
}
}
}
int q[N] , h = 1 , t = 0;
void spfa ( int s )
{
int ss = id[s];
for ( int i = 1 ; i <= tot ; i ++ ) dis[i] = 0;
q[++t] = ss , in[ss] = 1;
dis[ss] = scc[ss];
while ( h <= t )
{
int u = q[h++];
in[u] = 0;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( dis[v] < dis[u] + e[i].w )
{
dis[v] = dis[u] + e[i].w;
if ( !in[v] ) in[v] = 1 , q[++t] = v;
}
}
}
}
void init()
{
memset ( head , 0 , sizeof ( head ) );
cnt = 1;
}
signed main ()
{
n = read() , m = read();
for ( int i = 1 ; i <= m ; i ++ ) x[i] = read() , y[i] = read() , add ( x[i] , y[i] );
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
for ( int i = 1 ; i <= n ; i ++ ) if ( !dfn[i] ) tarjan(i);
init();
for ( int i = 1 ; i <= m ; i ++ )
if ( id[x[i]] != id[y[i]] ) add ( id[x[i]] , id[y[i]] , scc[id[y[i]]] );
int s = read() , p = read();
spfa ( s );
for ( int i = 1 ; i <= p ; i ++ )
ans = max ( ans , dis[id[read()]] );
printf ( "%d" , ans );
return 0;
}
E. 稳定婚姻
[题目描述]
我们已知 \(n\) 对夫妻的婚姻状况,称第 \(i\) 对夫妻的男方为 \(B_i\),女方为 \(G_i\)。若某男 \(B_i\) 与某女 \(G_j\) 曾经交往过(无论是大学,高中,亦或是幼儿园阶段,\(i \le j\)),则当某方与其配偶(即 \(B_i\) 与 \(G_i\) 或 \(B_j\) 与 \(G_j\))感情出现问题时,他们有私奔的可能性。不妨设 \(B_i\) 和其配偶 \(G_i\) 感情不和,于是 \(B_i\) 和 \(G_j\) 旧情复燃,进而 \(B_j\) 因被戴绿帽而感到不爽,联系上了他的初恋情人 \(Gk\) ……一串串的离婚事件像多米诺骨牌一般接踵而至。若在 \(B_i\) 和 \(G_i\) 离婚的前提下,这 \(2n\) 个人最终依然能够结合成 \(n\) 对情侣,那么我们称婚姻 \(i\) 为不安全的,否则婚姻 \(i\) 就是安全的。
给定所需信息,你的任务是判断每对婚姻是否安全。
[输入格式]
第一行为一个正整数 \(n\),表示夫妻的对数;
以下 \(n\) 行,每行包含两个字符串,表示这 \(n\) 对夫妻的姓名(先女后男),由一个空格隔开;
第 \(n+2\) 行包含一个正整数 \(m\),表示曾经相互喜欢过的情侣对数;
以下 \(m\) 行,每行包含两个字符串,表示这 \(m\) 对相互喜欢过的情侣姓名(先女后男),由一个空格隔开。
[输出格式]
输出文件共包含 \(n\) 行,第 \(i\) 行为 Safe
(如果婚姻 \(i\) 是安全的)或 Unsafe
(如果婚姻 \(i\) 是不安全的)。
[算法分析]
tarjan强连通分量实现(实际上二分图也可以实现 每一次拆散一对夫妻的配对\(link[link[i]]=0,link[i]=0\) 再从这个i出发进行匈牙利dfs 如果可以找到另一组匹配 则这段婚姻不安全
断边可以断双向边 但是需要提前记录nx和ny以便在匈牙利中不走这一边并在判断结束后还原连边关系
[代码实现]
二分图代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
const int inf = 0x3f3f3f3f;
inline int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , nx , ny;
int head[N] , cnt = 1;
struct node { int to , nxt , w; } e[20000005];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
map < string , int > a , b;
string s1 , s2;
int link[N] , vis[N];
int dfs ( int u )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( vis[v] || ( u == nx && v == ny ) ) continue;
vis[v] = 1;
if ( !link[v] || dfs ( link[v] ) ) return 1;
}
return 0;
}
signed main ()
{
n = read();
for ( int i = 1 ; i <= n ; i ++ )
{
cin >> s1 >> s2;
a[s1] = i , b[s2] = i + n;//女
add ( i , i + n );
link[i] = i+n , link[i+n] = i;
}
m = read();
for ( int i = 1 ; i <= m ; i ++ )
{
cin >> s1 >> s2;
add ( a[s1] , b[s2] );
}
for ( int i = 1 ; i <= n ; i ++ )
{
memset ( vis , 0 , sizeof ( vis ) );
nx = i , ny = link[i]; //标记删除的边
link[link[i]] = 0 , link[i] = 0;
if ( dfs ( i ) ) printf ( "Unsafe\n" );
else printf ( "Safe\n" );
link[ny] = nx , link[nx] = ny;
}
return 0;
}
F. 宫室宝藏
[题目描述]
在宽广的非洲荒漠中,生活着一群勤劳勇敢的羊驼家族。被族人恭称为“先知”的Alpaca L. Sotomon是这个家族的领袖,外人也称其为“所驼门王”。所驼门王毕生致力于维护家族的安定与和谐,他曾亲自率军粉碎河蟹帝国主义的野蛮侵略,为族人立下赫赫战功。所驼门王一生财宝无数,但因其生性节俭低调,他将财宝埋藏在自己设计的地下宫殿里,这也是今天Henry Curtis故事的起点。Henry是一个爱财如命的贪婪家伙,而又非常聪明,他费尽心机谋划了这次盗窃行动,破解重重机关后来到这座地下宫殿前。
整座宫殿呈矩阵状,由R×C间矩形宫室组成,其中有N间宫室里埋藏着宝藏,称作藏宝宫室。宫殿里外、相邻宫室间都由坚硬的实体墙阻隔,由一间宫室到达另一间只能通过所驼门王独创的移动方式——传送门。所驼门王为这N间藏宝宫室每间都架设了一扇传送门,没有宝藏的宫室不设传送门,所有的宫室传送门分为三种:
-
“横天门”:由该门可以传送到同行的任一宫室;
-
“纵寰门”:由该门可以传送到同列的任一宫室;
-
“任意门”:由该门可以传送到以该门所在宫室为中心周围8格中任一宫室(如果目标宫室存在的话)。
深谋远虑的Henry当然事先就搞到了所驼门王当年的宫殿招标册,书册上详细记录了每扇传送门所属宫室及类型。而且,虽然宫殿内外相隔,但他自行准备了一种便携式传送门,可将自己传送到殿内任意一间宫室开始寻宝,并在任意一间宫室结束后传送出宫。整座宫殿只许进出一次,且便携门无法进行宫室之间的传送。不过好在宫室内传送门的使用没有次数限制,每间宫室也可以多次出入。
现在Henry已经打开了便携门,即将选择一间宫室进入。为得到尽多宝藏,他希望安排一条路线,使走过的不同藏宝宫室尽可能多。请你告诉Henry这条路线最多行经不同藏宝宫室的数目。
[输入格式]
输入文件sotomon.in第一行给出三个正整数N, R, C。
以下N行,每行给出一扇传送门的信息,包含三个正整数xi, yi, Ti,表示该传送门设在位于第xi行第yi列的藏宝宫室,类型为Ti。Ti是一个1~3间的整数,1表示可以传送到第xi行任意一列的“横天门”,2表示可以传送到任意一行第yi列的“纵寰门”,3表示可以传送到周围8格宫室的“任意门”。
保证1≤xi≤R,1≤yi≤C,所有的传送门位置互不相同。
[输出格式]
输出文件sotomon.out只有一个正整数,表示你确定的路线所经过不同藏宝宫室的最大数目。
[算法分析]
tarjan缩点+建图优化
首先 对于传送门的连边我们容易想到以下方法:
- 对于每一个“横天门”,我们向同一行的所有宫室连出一条有向边。
- 对于每一个“纵寰门”,我们向同一列的所有宫室连出一条有向边。
- 对于每一个“任意门”,我们向“九宫格”内的另外八个宫室连出一条有向边。
显然需要优化 容易想到:没有宝藏的宫室,既没有对其他宫室的出边,也没有对答案有贡献的点权,可以忽略不计,不参与建图。
则建边方法转化为:
- 对于每一个“横天门”,我们向同一行的所有有宝藏的宫室连出一条有向边。
- 对于每一个“纵寰门”,我们向同一列的所有有宝藏的宫室连出一条有向边。
- 对于每一个“任意门”,我们向“九宫格”内的另外八个宫室中有宝藏的连出一条有向边。
这种建边方法还是不够优化 对于一行之内全是横向门的数据会退化到\(O(n^2)\) 考虑继续优化
我们新建\(n+r+c\)个节点 前r个表示每一行的代表节点 中间c个表示每一列的代表节点 后n个表示所有有宝藏的宫殿节点
那么,建边方式发生如下变化:
- 对于每一个“横天门”,我们向表示这一行的节点连出一条有向边。
- 对于每一个“纵寰门”,我们向表示这一列的节点连出一条有向边。
- 对于每一个“任意门”,我们向“九宫格”内的另外八个宫室中有宝藏的连出一条有向边。
- 对于节点 \(1 - R\),向这一行所有有宝藏的宫室连出一条有向边。
- 对于节点\(R+1-R+C\),向这一列所有有宝藏的宫室连出一条有向边。
对于任意门周围是否有宝藏节点可以使用stlmap存储
[代码实现]
自认为可读性极好的代码/kk