给定一张无向联通图 ( V , E ) (V,E) (V,E),对于每一个点 P ∈ V P\in V P∈V,求当 P P P不能被经过时满足 u , v u,v u,v不连通的有序点对数。
一道十分适合练习 T a r j a n Tarjan Tarjan的图论题。
对于每一个点,考虑将其删除对图会产生什么影响,进而计算答案。
不是割点:~~显然只是少了个点而已,~~答案就是 2 ( n − 1 ) 2(n-1) 2(n−1)。
是割点:还是少了一个点
删除割点将会导致整个图变成多个联通块。容易发现不同联通块内的点之间不能互相到达。
即大小为 s i z e a , s i z e b size_a,size_b sizea,sizeb的两个联通块对答案产生 2 s i z e a s i z e b 2 size_a size_b 2sizeasizeb的贡献。
那么设全图有 n n n个点,删除该割点产生 k k k个联通块,则答案就是联通块两两配对产生的贡献之和。
即 2 ( n − 1 ) + ∑ i = 1 k ∑ j = 1 , j ≠ i k s i z e i × s i z e j 2(n-1)+\sum_{i=1}^k \sum_{j=1,j\neq i}^{k}size_i \times size_j 2(n−1)+∑i=1k∑j=1,j̸=iksizei×sizej
这里需要 O ( n 2 ) O(n^2) O(n2)卷起来,遇到菊花图就凉了。。。
考虑优化,显然 ∑ j = 1 , j ≠ i k s i z e j = n − s i z e i − 1 \sum_{j=1,j\neq i}^k size_j =n-size_i-1 ∑j=1,j̸=iksizej=n−sizei−1
所以上式化为 2 ( n − 1 ) + ∑ i = 1 k s i z e i × ( n − s i z e i − 1 ) 2(n-1)+\sum_{i=1}^k size_i \times( n-size_i-1 ) 2(n−1)+∑i=1ksizei×(n−sizei−1)
不是割点可以认为割掉以后产生一个大小为 1 1 1的联通块,不必单独处理。
考虑 T a r j a n Tarjan Tarjan如何求联通块大小。
T a r j a n Tarjan Tarjan实际上是在 d f s dfs dfs生成树上,利用 d f n dfn dfn和 l o w low low对返祖边进行标记。
所以一个点的子树可以分成两类:存在返祖边或不存在。
对于前者,割掉该点并不影响连通性,所以和祖先算作一个联通块;
对于后者,割掉该点将使得其变为独立的联通块,所以在搜索时顺便计算 s i z e size size。
于是可以方便地算出后者的 s i z e size size之和 s u m sum sum,而前者总大小即为 n − s u m − 1 n-sum-1 n−sum−1。
在搜索时一边累加 s u m sum sum,一边累加答案,最后加上 n − 1 n-1 n−1,得到的是无序点对的个数。
输出时乘以二即可。。。
#include
#include
using std::freopen;
using std::fread;
using std::scanf;
using std::printf;
using std::min;
char gc()
{
static char buf[1<<18],*p1=buf,*p2=buf;
if(p1==p2)
{
p2=(p1=buf)+fread(buf,1,1<<18,stdin);
if(p1==p2)return EOF;
}
return *p1++;
}
template<typename _Tp>
void read(_Tp& x)
{
x=0;
char c=gc();
while(c<'0'||c>'9')c=gc();
while(c>='0'&&c<='9')
{
x=(x<<1)+(x<<3)+(c^48);
c=gc();
}
}
template<typename _Tp>
void write(_Tp x)
{
if(x>=10)write(x/10);
putchar((x%10)^48);
}
const int N=100005,M=500005;
int n,m;
int head[N],dfn[N],low[N];
int size[N];
long long ans[N];
struct Edge
{
int next,to;
};
Edge E[M<<1];
int tot;
void add(int u,int v)
{
E[++tot].next=head[u];
E[tot].to=v;
head[u]=tot;
}
int cnt;
void tarjan(int u)
{
dfn[u]=low[u]=++cnt;
long long sum=0;
size[u]=1;
for(int i=head[u];i;i=E[i].next)
{
int v=E[i].to;
if(!dfn[v])
{
tarjan(v,u);
size[u]+=size[v];
if(low[v]>=dfn[u])//判断不存在返祖边的情况
{
ans[u]+=size[v]*sum;//计算这一棵子树的贡献
sum+=size[v];
}
low[u]=min(low[u],low[v]);
}
else
{
low[u]=min(low[u],dfn[v]);
}
}
ans[u]+=(n-sum-1)*sum;//别忘了计算剩下的点产生的贡献!
ans[u]+=n-1;
}
int main()
{
read(n),read(m);
for(int i=0;i<m;++i)
{
int u,v;
read(u),read(v);
add(u,v);
add(v,u);
}
tarjan(1);
for(int i=1;i<=n;++i)
{
write(ans[i]*2);//之前求出无序点对,这里乘2
putchar('\n');
}
}
P.S. 这题被我校ZJOI2018 rk2神仙拿来出模拟赛,结果数据出现负数导致几乎所有原本A掉这题的人爆50。。。