[省选联考 2021 A/B 卷] 图函数

一、题目

点此看题

开始接受(...)痛苦不堪的回忆。

二、解法

你看它不用算具体的东西,只用算一个总和,这不用贡献法用什么?

考虑 (v) 的贡献,也就是保留 ([v,n]) 的点和有关边时,和它能互通 (u) 点的个数。前 ([1,v)) 不用考虑是因为如果和 (u) 能互通就会删除,如果不互通那么和 (u) 不在一个强连通块内,对 (u,v) 是否能互通没有任何影响。

枚举点 (v),然后跑 ( t tarjan),单次可以做到 (O(nm)) 的复杂度。

先固定点 (v),考虑删除一个边的前缀的影响,把删边变成加边是老套路了,我们逆序来做。( t tarjan) 不好维护但考虑到有一个点是定点,所以建出正反图,然后动态 ( t bfs),如果加入的这条边两个端点都访问过就没用,如果终点没有访问过就以他开始 ( t bfs),每次把 ( t bfs) 到的边删掉,如果两个端点都没访问过就加入图中。每条边只会被删除一次,所以时间复杂度 (O(nm))


还有一种方法是考虑点对 ((u,v)) 的贡献,也就是保留 ([v,n]) 的点和边时 (u,v) 能互通。因为本题边越多 (u,v) 更容易互通,所以我们给每个边一个时间(第 (i) 条边时间为 (i)),可以考虑求出 (u,v) 互通的最小瓶颈边最大的路径,那么可知这会贡献给答案序列的一个前缀。

有向图的瓶颈边问题可以考虑 ( t floyd),设 (f[i][j]) 表示 (i)(j) 的最小瓶颈边的最大值,那么考虑枚举中转点 (k),有一个限制是 (kgeqmin(i,j)),因为图只能保留这些点,在内层循环的时候注意一下即可。

还有一个细节是 (k) 要倒序枚举,首先考虑 ( t floyd) 的原理:对于每一条可能的路径,我们通过枚举中转点能把这些点一个一个拼起来,这相当于枚举了所有情况。但是这道题的路径可能是 small->big->middle->big->small,如果先枚举了small就会导致路径拼不起来,所以要先枚举big

时间复杂度 (O(n^3)),真 ( t tm) 怎么做都可以,但是考试时就是不会。

三、总结

当不用求出具体值,只用求总和时,考虑贡献法。

尝试多种翻译方式,比如这道题我一开始翻译的是 ((u,v)) 强联通,但是因为 ( t tarjan) 不好动态做所以卡了。但是如果翻译成 ((u,v)) 互通就利于后面想到动态 ( t bfs) 的方法。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,f[M][M],ans[200005];
signed main()
{
    n=read();m=read();
    for(int i=1;i<=m;i++)
    {
        int u=read(),v=read();
        f[u][v]=i;
    }
    for(int k=n;k>=1;k--)
    {
        for(int i=1;i<=n;i++)
        {
            if(!f[i][k]) continue;
            int t=f[i][k],up=(i>k)?(k-1):n;
            for(int j=1;j<=up;j++)
                f[i][j]=max(f[i][j],min(t,f[k][j]));
        }
    }
    for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++)
            ans[min(f[i][j],f[j][i])]++;
    ans[m+1]=n;
    for(int i=m;i>=1;i--) ans[i]+=ans[i+1];
    for(int i=1;i<=m+1;i++) printf("%d ",ans[i]);
}

原文地址:https://www.cnblogs.com/C202044zxy/p/15024721.html