学习笔记:最小生成树

上讲习题

AcWing 1126

不难发现,想要用的钱尽量少,转账过程中交的手续费就要尽量少。因为我们是到达最后要得到 100 100 100元,所以可以从 t t t s s s求最短路。注意,每一条边的权是转完账后剩余的钱是转账之前的钱的几分之几,即 w = 1 − z ÷ 100 w=1-z\div 100 w=1z÷100。因为是反着走,所以如果要求原来的钱就是除以 w w w

#include<bits/stdc++.h>
using namespace std;
const int NN=2004;
vector<pair<int,double> >g[NN];
double d[NN];
bool vis[NN];
void dij(int s)
{
    
    
    d[s]=100;
    priority_queue<pair<double,int>,vector<pair<double,int> >,greater<pair<double,int> > >q;
    q.push({
    
    d[s],s});
    while(q.size())
    {
    
    
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
    
    
            int v=g[t][i].first;
            double w=g[t][i].second;
            if(d[v]>d[t]/w)
            {
    
    
                d[v]=d[t]/w;
                if(!vis[v])
                    q.push({
    
    d[v],v});
            }
        }
    }
}
int main()
{
    
    
	int n,m,s,e;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
    
    
		int u,v;
		double w;
        scanf("%d%d%lf",&u,&v,&w);
        w=1-w/100.0;
        g[u].push_back({
    
    v,w});
        g[v].push_back({
    
    u,w});
	}
	scanf("%d%d",&s,&e);
	for(int i=1;i<=n;i++)
	    d[i]=1e9;
	dij(e);
	printf("%.8lf",d[s]);
	return 0;
}

AcWing 903

把走到某一个点想象成得到了这个点的物品,那么能优惠就相当于现在如果你获得了 u u u,那么你就可以再花 w w w的代价得到 i i i,从 u u u i i i连一条权为 w w w的边即可。每个东西可以直接买,可以直接从一个虚拟源点走到这里获得这个物品,从虚拟源点连一条权为 p i p_i pi的边。考虑地位问题,我们知道,如果和地位差大于 m m m的人交易那么就是徒劳。所以,只能在地位差小于 m m m的人里交易。首先,我们要满足酋长的要求,于是地位必须在 a 1 − m 、 a 1 + m a_1-m、a_1+m a1ma1+m的区间内交易。然后,为了保证其他的所有部落的地位差都不会超过 m m m,所以区间的长度最大为 m m m,枚举这个区间即可。注意,本题是稠密图,用不堆优化的版本更快。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,INF=0x3f3f3f3f;
int m,n,w[NN][NN],a[NN],d[NN];
bool vis[NN];
int dj(int down,int up)
{
    
    
    memset(d,0x3f,sizeof(d));
    memset(vis,0,sizeof(vis));
    d[0]=0;
    for(int i=1;i<=n+1;i++)
	{
    
    
        int t=-1;
        for(int j=0;j<=n;j++)
            if(!vis[j]&&(t==-1||d[t]>d[j]))
                t=j;
        vis[t]=true;
        for(int j=1;j<=n;j++)
            if(a[j]>=down&&a[j]<=up)
                d[j]=min(d[j],d[t]+w[t][j]);
    }
    return d[1];
}
int main()
{
    
    
    scanf("%d%d",&m,&n);
    memset(w,0x3f,sizeof(w));
    for(int i=1;i<=n;i++)
		w[i][i]=0;
    for(int i=1;i<=n;i++)
	{
    
    
        int p,cnt;
        scanf("%d%d%d",&p,&a[i],&cnt);
        w[0][i]=min(w[0][i],p);
        while(cnt--)
		{
    
    
            int u,x;
            scanf("%d%d",&u,&x);
            w[u][i]=min(w[u][i],x);
        }
    }
    int res=INF;
    for(int i=a[1]-m;i<=a[1];i++)
		res=min(res,dj(i,i+m));
    printf("%d",res);
    return 0;
}

AcWing 340

本题可以二分所需要支付的钱,如果小于等于这个支付的钱那么就可以将其选择成要购买的边;如果大于,那么就必须使用一个免费建设的机会。一遍最短路求一下怎么走需要免费建设的边才会最少,如果最小的方案需要免费建设的边数小于等于 k k k,那么这个支付的金额就是可行的,反之不行。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
vector<pair<int,int> >g[NN];
int d[NN],n,m,k;
bool vis[NN];
bool check(int mid)
{
    
    
    memset(d,0x3f,sizeof(d));
    memset(vis,false,sizeof(vis));
    deque<int>q;
    q.push_back(1);
    d[1]=0;
    while(q.size())
    {
    
    
        int t=q.front();
        q.pop_front();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
    
    
            int v=g[t][i].first,w=g[t][i].second>mid;
            if(d[v]>d[t]+w)
            {
    
    
                d[v]=d[t]+w;
                if(!vis[v])
                {
    
    
                    if(!w)
                        q.push_front(v);
                    else
                        q.push_back(v);
                }
            }
        }
    }
    return d[n]<=k;
}
int main()
{
    
    
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({
    
    v,w});
        g[v].push_back({
    
    u,w});
    }
    int l=0,r=1e9;
    while(l<r)
    {
    
    
        int mid=l+(r-l)/2;
        if(check(mid))
            r=mid;
        else
            l=mid+1;
    }
    if(r==1e9)
        printf("-1");
    else
        printf("%d",r);
    return 0;
}

AcWing 341

一条路径可以分为两部分,前半部分和后半部分。可以假设在前半部分买入,在后半部分卖出。设 f i f_i fi表示从 s s s i i i中水晶球价格最小的城市,表示从这里买入, g i g_i gi表示从 i i i t t t中水晶球价格最高的城市,表示从这里卖出。这样,枚举每一个点作为路径的划分点,则答案就是 min ⁡ ( g i − f i ) \min(g_i-f_i) min(gifi)。因为第二个是单汇最短路,可以建立反向边从 t t t算最短路。

#include<bits/stdc++.h>
using namespace std;
const int NN=1000004;
int n,m,ans,f[NN],g[NN],a[NN];
bool vis[NN];
struct node1
{
    
    
    int head[NN],e[NN],nxt[NN],idx;
    void add(int u,int v)
    {
    
    
        e[++idx]=v;
        nxt[idx]=head[u];
        head[u]=idx;
    }
    void spfa(int s)
    {
    
    
        memset(vis,false,sizeof(vis));
        memset(f,0x3f,sizeof(f));
        queue<int>q;
        q.push(s);
        f[s]=a[s];
        while(q.size())
        {
    
    
            int t=q.front();
            q.pop();
            vis[t]=false;
            for(int i=head[t];i;i=nxt[i])
            {
    
    
                int v=e[i],w=min(f[t],a[v]);
                if(f[v]>w)
                {
    
    
                    f[v]=w;
                    if(!vis[v])
                    {
    
    
                        vis[v]=true;
                        q.push(v);
                    }
                }
            }
        }
    }
}g1;
struct node2
{
    
    
    int head[NN],e[NN],nxt[NN],idx;
    void add(int u,int v)
    {
    
    
        e[++idx]=v;
        nxt[idx]=head[u];
        head[u]=idx;
    }
    void spfa(int s)
    {
    
    
        memset(vis,false,sizeof(vis));
        memset(g,-0x3f,sizeof(f));
        queue<int>q;
        q.push(s);
        g[s]=a[s];
        while(q.size())
        {
    
    
            int t=q.front();
            q.pop();
            vis[t]=false;
            for(int i=head[t];i;i=nxt[i])
            {
    
    
                int v=e[i],w=max(g[t],a[v]);
                if(g[v]<w)
                {
    
    
                    g[v]=w;
                    if(!vis[v])
                    {
    
    
                        vis[v]=true;
                        q.push(v);
                    }
                }
            }
        }
    }
}g2;
int main()
{
    
    
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,opt;
        scanf("%d%d%d",&u,&v,&opt);
        if(opt==1)
        {
    
    
            g1.add(u,v);
            g2.add(v,u);
        }
        else
        {
    
    
            g1.add(u,v);
            g1.add(v,u);
            g2.add(u,v);
            g2.add(v,u);
        }
    }
    g1.spfa(1);
    g2.spfa(n);
    for(int i=1;i<=n;i++)
        ans=max(ans,g[i]-f[i]);
    printf("%d\n",ans);
    return 0;
}

AcWing 345

本题就是一个 b f s bfs bfs搜路径的问题。把某一个点的钥匙和当前已经获得的钥匙的信息二进制压缩存储即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=14,dx[4]={
    
    1,-1,0,0},dy[4]={
    
    0,0,1,-1};
int vis[NN][NN][1<<NN],st[NN][NN][NN][NN],key[NN][NN],n,m;
struct node
{
    
    
	int x,y,key,d;
};
int bfs()
{
    
    
    queue<node>q;
	q.push((node){
    
    1,1,key[1][1],0});
	vis[1][1][key[1][1]]=1;
	while(!q.empty())
	{
    
    
		node u=q.front();
		q.pop();
		for(int i=0;i<4;i++)
		{
    
    
			int nx=u.x+dx[i],ny=u.y+dy[i];
			if(nx<1||ny<1||nx>n||ny>m)
				continue;
			if(st[u.x][u.y][nx][ny]==-1)
				continue;
			if((st[u.x][u.y][nx][ny]&u.key)!=st[u.x][u.y][nx][ny])
				continue;
			if(vis[nx][ny][u.key|key[nx][ny]])
				continue;
			if(nx==n&&ny==m)
				return u.d+1;
			vis[nx][ny][u.key|key[nx][ny]]=true;
			q.push((node){
    
    nx,ny,u.key|key[nx][ny],u.d+1});
		}
	}
	return -1;
}
int main()
{
    
    
	int k;
	scanf("%d%d%*d%d",&n,&m,&k);
	while(k--)
	{
    
    
		int x1,y1,x2,y2,z;
		scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&z);
		st[x1][y1][x2][y2]=st[x2][y2][x1][y1]=!z?-1:1<<z-1;
	}
	scanf("%d",&k);
	while(k--)
	{
    
    
		int x,y,p;
		scanf("%d%d%d",&x,&y,&p);
		key[x][y]|=1<<p-1;
	}
	printf("%d",bfs());
	return 0;
}

AcWing 383

只多一个航程,肯定是次小方案,所以可以统计到每个点的最短和次短距离,最后再看看是否差一个航程,那么就可以累加次大的方案。思考如何迭代,老方法,如果从某一个点的最短路或次短路迭代可以得到当前点的最短路或次短路,那么当前点的最短路或次短路的方案数就加上那个点的最短路或次短路的方案。如果更新的最短路大于了原来的最短路,那么更新最大和次大即可,如果大于了次短路,那么更新次短路即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int d[NN][2],cnt[NN][2];
vector<pair<int,int> >g[NN];
int main()
{
    
    
    int t;
    scanf("%d",&t);
    while(t--)
    {
    
    
        for(int i=1;i<=NN;i++)
            g[i].clear();
        memset(d,0x3f,sizeof(d));
        memset(cnt,0,sizeof(cnt));
    	int n,m;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++)
        {
    
    
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            g[u].push_back({
    
    v,w});
        }
        int s,f;
        scanf("%d%d",&s,&f);
        d[s][0]=0;
        cnt[s][0]=1;
        priority_queue<pair<int,pair<int,int> >,vector<pair<int,pair<int,int> > >,greater<pair<int,pair<int,int> > > >q;
        q.push({
    
    0,{
    
    s,0}});
        while(!q.empty())
        {
    
    
            pair<int,pair<int,int> >u=q.top();
            q.pop();
            if(d[u.second.first][u.second.second]!=u.first)
				continue;
            for(int i=0;i<g[u.second.first].size();i++)
            {
    
    
                pair<int,int> v=g[u.second.first][i];
                if(d[v.first][0]>u.first+v.second)
                {
    
    
                    d[v.first][1]=d[v.first][0];
                    cnt[v.first][1]=cnt[v.first][0];
                    q.push({
    
    d[v.first][1],{
    
    v.first,1}});
                    d[v.first][0]=u.first+v.second;
                    cnt[v.first][0]=cnt[u.second.first][u.second.second];
                    q.push({
    
    d[v.first][0],{
    
    v.first,0}});
                }
                else if(d[v.first][0]==u.first+v.second)
                    cnt[v.first][0]+=cnt[u.second.first][u.second.second];
                else if(d[v.first][1]>u.first+v.second)
                {
    
    
                    d[v.first][1]=u.first+v.second;
                    cnt[v.first][1]=cnt[u.second.first][u.second.second];
                    q.push({
    
    d[v.first][1],{
    
    v.first,1}});
                }
                else if(d[v.first][1]==u.first+v.second)
                    cnt[v.first][1]+=cnt[u.second.first][u.second.second];
            }
        }
        printf("%d\n",cnt[f][0]+(d[f][1]==d[f][0]+1?cnt[f][1]:0));
    }
    return 0;
}

AcWing 345

现在给出本题法二,也就是正解。要看法一的点我, b e l l m a n − f o r d bellman-ford bellmanford的例题就是。回到正解,我们发现本题要求的恰好的边数太大了,但是又不能避免计算它,则考虑倍增。但是 b e l l m a n − f o r d bellman-ford bellmanford一次只能扩展一条边,没办法两倍两倍地扩展。不难想起来, F l o y d Floyd Floyd的状态转移是两个路径,而不是用边来转移,则可以用 F l o y d Floyd Floyd来处理这个问题。考虑如何适用它,因为一次只能用两条路径来扩展,所以把 k k k放在最后一层循环即可。注意,我们只是改成了 F l o y d Floyd Floyd,但是照样要强制转移,因为只有这样,使用的边数才会是 2 2 2的幂次增长,而且我们求的也是恰好。

#include<bits/stdc++.h>
using namespace std;
const int NN=104;
struct node
{
    
    
    int a[NN][NN];
    void init()
	{
    
    
        memset(a,0x3f,sizeof(a));
    }
};
int idx;
node mul(node a,node b)
{
    
    
    node ans;
    ans.init();
    for(int i=1;i<=idx;i++)
        for(int j=1;j<=idx;j++)
            for(int k=1;k<=idx;k++)
                ans.a[i][j]=min(ans.a[i][j],a.a[i][k]+b.a[k][j]);
    return ans;
}
node ksm(node a,int x)
{
    
    
    node ans;
    memcpy(ans.a,a.a,sizeof(ans.a));
    x--;
    while(x)
	{
    
    
        if(x&1)
			ans=mul(ans,a);
        a=mul(a,a);
        x>>=1;
    }
    return ans;
}
int main()
{
    
    
    int n,t,s,e;
    scanf("%d%d%d%d",&n,&t,&s,&e);
    node a;
    a.init();
	map<int,int>mp;
    for(int i=1;i<=t;i++)
	{
    
    
        int u,v,w;
        scanf("%d%d%d",&w,&u,&v);
        u=mp[u]?mp[u]:(mp[u]=++idx);
        v=mp[v]?mp[v]:(mp[v]=++idx);
        a.a[u][v]=a.a[v][u]=min(a.a[u][v],w);
    }
    node ans=ksm(a,n);
    printf("%d\n",ans.a[mp[s]][mp[e]]);
    return 0;
}

概念

生成树,最短路的一个问题。即指在一个无向图中选择一些边,使得所有点连通且没有环。
最小生成树,故名思意,就是生成树中最小的一个。

算法

Prim算法

方法

P r i m Prim Prim算法的本质是贪心,适用于稠密图。先将任意一点的权设为 0 0 0,其余的设为正无穷。然后每次找一个权值最小且没有被标记的点,答案加上该点的点权并标记该点,用这个点到 v v v的边权和 v v v的点权取更小值更新 v v v的点权,执行 n n n次。标记一个点可以理解为将这个点加入了树上的一个点。算法过程可以看成找目前可以新加为树枝边的边中最短的一条加入树枝边,并将另一端的点加入树。
时间复杂度 Θ ( n 2 ) \Theta(n^2) Θ(n2)
其实 P r i m Prim Prim算法也有堆优化版本,时间复杂度变成了 O ( m log ⁡ n ) O(m\log n) O(mlogn),适用于稀疏图。

例题

AcWing 1140

就是最小生成树的板子。本题是稠密图,用不堆优化的 P r i m Prim Prim更优。

#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int g[NN][NN],d[NN];
bool vis[NN];
int main()
{
    
    
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			scanf("%d",&g[i][j]);
	int ans=0;
	memset(d,0x3f,sizeof(d));
	d[1]=0;
	for(int i=1;i<=n;i++)
	{
    
    
	    int u=0;
	    for(int j=1;j<=n;j++)
	        if(!vis[j]&&d[j]<d[u])
	            u=j;
        ans+=d[u];
        vis[u]=true;
        for(int j=1;j<=n;j++)
            if(!vis[j])
                d[j]=min(d[j],g[u][j]==0?(int)1e9:g[u][j]);
	}
	printf("%d",ans);
	return 0;
}

Kruskal算法

方法

K r u s k a l Kruskal Kruskal算法的本质也是贪心,适用稀疏图。把每条边按长度排序,依次枚举每一条边。如果所连接的两边分别属于两个连通块,就将其加入树边,直到加入了 n − 1 n-1 n1条边停止。最开始每个点分别是一个连通块。算法过程可以看成如果这条边连接了两棵树就合并这两棵树,直到合并成了一个树为止。
平均时间复杂度 Θ ( n + m log ⁡ m ) \Theta(n+m\log m) Θ(n+mlogm),总时间复杂度 O ( n m 2 ) O(nm^2) O(nm2)。算法本身时间复杂度不高,是 Θ ( n + m ) \Theta(n+m) Θ(n+m)的,主要都是快排的时间慢。
虽然 K r u s k a l Kruskal Kruskal的时间复杂度比堆优化的 P r i m Prim Prim慢,但是 K r u s k a l Kruskal Kruskal容易对于不同的题目来变化, P r i m Prim Prim就比较难。

例题

AcWing 1141

本题是稀疏图,可以用 K r u s k a l Kruskal Kruskal做。但是题目并没有说连通,会不会出现问题呢?当然不会,因为题目说了:

使得网络中没有回路且不影响连通性(即如果之前某两个点是连通的,去完之后也必须是连通的)

所以我们只需要保证连通性不变即可。因为 K r u s k a l Kruskal Kruskal算法选择一条边当且仅当这条边连接了两个连通块,所以原图中是同一个连通块的两个点,在新图中一定会用这条边或者其他边连接起来,还是属于同一个连通块。

#include<bits/stdc++.h>
using namespace std;
struct node
{
    
    
    int u,v,w;
    bool operator<(const node&it)const
    {
    
    
        return w<it.w;
    }
}edge[204];
int fa[104];
int find(int x)
{
    
    
    return fa[x]==x?x:find(fa[x]);
}
int main()
{
    
    
    int n,k,sum=0;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=k;i++)
    {
    
    
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[i]={
    
    u,v,w};
        sum+=w;
    }
    for(int i=1;i<=n;i++)
        fa[i]=i;
    sort(edge+1,edge+1+k);
    int res=0;
    for(int i=1;i<=k;i++)
    {
    
    
        int fu=find(edge[i].u),fv=find(edge[i].v);
        if(fu!=fv)
        {
    
    
            fa[fu]=fv;
            res+=edge[i].w;
        }
    }
    printf("%d",sum-res);
    return 0;
}

最小瓶颈树

概念

最小瓶颈树,就是指选出来的边中最大边最小的生成树。

方法

最小生成树等于最小瓶颈树,证明如下:假设使用 K r u s k a l Kruskal Kruskal算法得到了一个最小生成树,但是它不是最小瓶颈树。则会有一条更小的边加上之后和最大的边组成了环,这样就可以去掉最大的边。可是在 K r u s k a l Kruskal Kruskal中会从小到大把所有边枚举,一旦连接了两个连通块就会作为树边,所以一定不存在一个可以和最大边组成环的一个更小的边,与假设矛盾,得证。

例题

AcWing 1142

连接了所有点的最优方案一定是树,所以本题就是求最小瓶颈树,直接求最小生成树,在最小生成树上找最大边即可。要求输出选出来的边数,树的边数等于点数减一。

#include<bits/stdc++.h>
using namespace std;
struct node
{
    
    
    int u,v,w;
    bool operator<(const node&it)const
    {
    
    
        return w<it.w;
    }
}edge[8004];
int fa[304];
int find(int x)
{
    
    
    return fa[x]==x?x:find(fa[x]);
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edge[i]={
    
    u,v,w};
    }
    for(int i=1;i<=n;i++)
        fa[i]=i;
    sort(edge+1,edge+1+m);
    int ans=0,k=0;
    for(int i=1;i<=m;i++)
    {
    
    
        int fu=find(edge[i].u),fv=find(edge[i].v);
        if(fu!=fv)
        {
    
    
            fa[fu]=fv;
            k++;
            ans=max(ans,edge[i].w);
        }
        if(k==n-1)
            break;
    }
    printf("%d %d",n-1,ans);
    return 0;
}

最小生成树的应用

AcWing 1143

有强制选边,在输入时直接加上即可。剩下的直接求最小生成树。

#include<bits/stdc++.h>
using namespace std;
struct node
{
    
    
    int u,v,w;
    bool operator<(const node&it)const
    {
    
    
        return w<it.w;
    }
}edge[100004];
int fa[2004];
int find(int x)
{
    
    
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
int main()
{
    
    
    int n,m,ans=0,k=0,cnt=0;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        fa[i]=i;
    for(int i=1;i<=m;i++)
    {
    
    
        int opt,u,v,w;
        scanf("%d%d%d%d",&opt,&u,&v,&w);
        if(opt==1)
        {
    
    
            int fx=find(u),fy=find(v);
            if(fx!=fy)
            {
    
    
                fa[fx]=fy;
                cnt++;
            }
            ans+=w;
        }
        else
            edge[++k]={
    
    u,v,w};
    }
    sort(edge+1,edge+1+k);
    for(int i=1;i<=k;i++)
    {
    
    
        if(cnt==n-1)
            break;
        int fx=find(edge[i].u),fy=find(edge[i].v);
        if(fx!=fy)
        {
    
    
            fa[fx]=fy;
            ans+=edge[i].w;
            cnt++;
        }
    }
    printf("%d",ans);
    return 0;
}

AcWing 1146

法一

本题中每个点加入了点权,很不好处理。稍作思考,想起来 P r i m Prim Prim算法就是以每个点相邻的树边的边权作为点权,现在多了一种直接选择点权而加入树的办法,这不正对 P r i m Prim Prim的胃口吗?于是,最开始直接把每个点的点权输入到 P r i m Prim Prim算法的每个点的点权即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=304;
int g[NN][NN],d[NN];
bool vis[NN];
int main()
{
    
    
    int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&d[i]);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		{
    
    
			scanf("%d",&g[i][j]);
		    if(i==j)
		        g[i][j]=1e9;
		}
	int ans=0;
	d[0]=1e9;
	for(int i=1;i<=n;i++)
	{
    
    
		int k=0;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&d[k]>d[j])
				k=j;
		ans+=d[k];
		vis[k]=true;
		for(int j=1;j<=n;j++)
			if(!vis[j])
				d[j]=min(d[j],g[k][j]);
	}
	printf("%d",ans);
	return 0;
}

法二

法二就是用 K r u s k a l Kruskal Kruskal的做法。因为本题是稠密图,所以这个方法反而会更慢,但是这个方法容易理解一些。每个点可以建立发电站,其实就相当于一个电厂直接给这个点电,那么设一个源点 0 0 0,从 0 0 0向每个点连一条权为该点的点权的边求最小生成树即可。

#include<bits/stdc++.h>
using namespace std;
struct node
{
    
    
    int u,v,w;
    bool operator<(const node&it)const
    {
    
    
        return w<it.w;
    }
}edge[100004];
int fa[2004];
int find(int x)
{
    
    
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
int main()
{
    
    
    int n,m=0;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
    
    
        int x;
        scanf("%d",&x);
        edge[++m]={
    
    0,i,x};
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
    
    
            int x;
            scanf("%d",&x);
            if(i!=j)
                edge[++m]={
    
    i,j,x};
        }
    for(int i=0;i<=n;i++)
        fa[i]=i;
    sort(edge+1,edge+1+m);
    int ans=0,k=0;
    for(int i=1;i<=m;i++)
    {
    
    
        int fx=find(edge[i].u),fy=find(edge[i].v);
        if(fx!=fy)
        {
    
    
            fa[fx]=fy;
            ans+=edge[i].w;
            k++;
        }
        if(k==n)
            break;
    }
    printf("%d",ans);
    return 0;
}

AcWing 1148

本题就是严格次小生成树。次小的,一定是找出一条较大的边换掉所组成的环上次大的边(即最小生成树在环上最大的边),于是现在只需要解决两个问题:1、找到最小生成树在环上最大的边;2、用哪条边换满足题目要求。先解决第一个问题,如果能和某条边组成环,那么这些边一定是连续的路径,暴搜找任意路径的最大边权即可。第二个问题,这两条边的差最小且不为 0 0 0就是最优方案,最后输出答案就是最小生成树的权加上这个差。注意,有可能会出现某条路径的最大边等于新加的边,但是用这个路径的次大边换就能得到次小生成树的情况。所以在计算路径的最大边权时还要计算路径的严格次大边权,如果最大边权等于新加的边的边权就用严格次大边换。

#include<bits/stdc++.h>
using namespace std;
const int NN=504,MM=20004;
struct node
{
    
    
    int u,v,w;
    bool vis;
    bool operator<(const node&it)const
    {
    
    
        return w<it.w;
    }
}edge[MM];
int fa[NN],d1[NN][NN],d2[NN][NN],h[NN],e[MM],w[MM],ne[MM],idx,n,m;
long long ans=1e18;
int find(int u)
{
    
    
    return fa[u]==u?u:fa[u]=find(fa[u]);
}
void add(int u,int v,int c)
{
    
    
    e[++idx]=v;
    w[idx]=c;
    ne[idx]=h[u];
    h[u]=idx;
}
void dfs(int u,int f,int maxx,int maxy,int d1[],int d2[])
{
    
    
    d1[u]=maxx;
	d2[u]=maxy;
    for(int i=h[u];i;i=ne[i])
        if(e[i]!=f)
        {
    
    
            int t1=maxx,t2=maxy;
            if(w[i]>t1)
			{
    
    
				t2=t1;
				t1=w[i];
			}
            else if(w[i]<t1&&w[i]>t2)
				t2=w[i];
            dfs(e[i],u,t1,t2,d1,d2);
        }
}
int main()
{
    
    
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
    sort(edge+1,edge+1+m);
    for(int i=1;i<=n;i++)
        fa[i]=i;
    long long sum=0;
    for(int i=1;i<=m;i++)
    {
    
    
        int fx=find(edge[i].u),fy=find(edge[i].v);
        if(fx!=fy)
        {
    
    
            fa[fx]=fy;
            sum+=edge[i].w;
            add(edge[i].u,edge[i].v,edge[i].w);
            add(edge[i].v,edge[i].u,edge[i].w);
            edge[i].vis=true;
        }
    }
    for(int i=1;i<=n;i++)
        dfs(i,0,0,0,d1[i],d2[i]);
    for(int i=1;i<=m;i++)
        if(!edge[i].vis)
        {
    
    
            long long res=sum+edge[i].w;
            if(edge[i].w>d1[edge[i].u][edge[i].v])
                res-=d1[edge[i].u][edge[i].v];
            else if(edge[i].w>d2[edge[i].u][edge[i].v])
                res-=d2[edge[i].u][edge[i].v];
            ans=min(ans,res);
        }
    printf("%lld",ans);
    return 0;
}

习题

AcWing 1144

AcWing 1145

AcWing 346

解析和代码在下一篇博客——负环给出

猜你喜欢

转载自blog.csdn.net/weixin_44043668/article/details/109026779