题意
每个人都从家里开车出来去公园,去公园的路上可能经过别人的家里,这样子可以蹭别人的车,自己的车就不用费油了,假设一辆车子可以无限坐人,每个人家里可以停无限量车,但是公园只能最多停k辆车,求所有人都到达公园且所有车子走过的路程之和最小为多少。
输入
第一行一个整数n
接下来n行每行两个字符串name1,name2和一个数字z,表示name1家到name2家又一条路径权值为z的路,其中名字为park的是公园,也就是终点
最后一行一个整数k表示公园能够停车的数量
输出
Total miles driven: x
x为所有车子走过的路程总和
解题思路
如果公园可以无限停车那么求最小生成树就好了,但是终点停车数量有限制,需要转换思路,这里假设公园为终点。先想一种一定符合条件的生成树(不一定是最小生成树),这样的生成树可以按如下前两步:
1.去掉终点跑一次 Kruskal 得到若干个连通块的最小生成树(并查集区分连通块)。
2.将终点和每个连通块相连,相连的边取最短的边,这样子就一定是一个符合条件的生成树了。
这里前两步就够造了一个可行的生成树
hint:假设第一步去掉终点跑一次Kruskal之后得到的连通块数量为m个,那么第二步与连通块连的最短的边为m条,这m条一定是最小生成树的边。终点最多停K辆车,这样子还可以用终点与连通块中的点连最多K-m条边使得生成树的权值之和变的更小。
3.对于当前已经构造的生成树中通过dp将从终点到当前节点i路径上的最长边存储在dp数组中(不包括包含终点的边),同时也需要存储这条边的起点和终点。
4.不断重复第三步,直到更新了K-m条边或者没办法更新为止。
还没有懂可以看下面代码+注释
#include<iostream>
#include<map>
#include<algorithm>
#include<string.h>
using namespace std;
struct node//存储边信息
{
int x,y,z;
node(){};
node(int a,int b,int c):x(a),y(b),z(c){};
bool operator <(const node &e) const{
return z<e.z;}
}edge[500];
const int maxn=22;
const int inf=0x3f3f3f3f;//无穷大
int g[maxn][maxn];//邻接矩阵存图
int tree[maxn][maxn];//存储当前生成树中有哪些边,tree[x][y]=1表示当前生成树中有e(x,y)这条边否则没有
int fa[maxn];//并查集存储连通块的根节点
int minedge[maxn];//minedge[x]!=inf时有效,此时x一定是某个连通块的根节点,minedge[x]表示终点到该节点的最短边长度
int linkpoint[maxn];//linkpoint[x]!=0时有效,此时x一定是某个连通块的根节点,linkpoint[x]表示重点到该节点最短边对应的节点
int ans,n,cnt,tot,m,k;
//ans表示所有车子走过的路径和
//n为题目中的n
//cnt为节点数
//tot为边数
//m为去掉终点后跑一次Krustra得到的连通块数量
//k为终点可以停的车子数量
node dp[maxn];//dp[x]表示从终点到点x路径上最长的边的信息(路径不包括包含终点的边)
map<string,int>mp;//输入时节点按照字符串输入,mp表示对应编号
int find(int x)//并查集
{
return x==fa[x]?x:(fa[x]=find(fa[x]));
}
void Kruskal()//第一步,除去终点跑一次Krustra
{
sort(edge+1,edge+1+tot);
for(int i=1;i<=tot;i++)
{
int x=edge[i].x;
int y=edge[i].y;
int z=edge[i].z;
if(x==1||y==1)//终点编号设置为1,所以包含1时跳过
continue;
if(find(x)!=find(y))
{
fa[find(x)]=find(y);
tree[x][y]=1;
tree[y][x]=1;
ans+=z;
}
}
}
void dfs(int cur,int pre)//从终点开始边dfs边找到终点到各个节点对应路径上权值最大的边,cur表示当前节点,pre表示前一个节点
{
for(int i=2;i<=cnt;i++)
{
if(i==pre||!tree[cur][i])//因为在当前构造生成树走的路径中找最长的边,而不是所有边中找,所以tree[cur][i]需要=1
continue;
if(dp[i].z==-1)//所有节点只会处理到一次,因为处理出来的生成树是一棵树
{
if(dp[cur].z>g[cur][i])
{
dp[i]=dp[cur];
}else
{
dp[i].x=cur;
dp[i].y=i;
dp[i].z=g[cur][i];
}
}
dfs(i,cur);
}
}
void parktoother()//包含2,3,4步
{
for(int i=2;i<=cnt;i++)//第二部,处理出终点到各个连通块的最小值,最小值信息放在各个连通块的根节点x上
{
if(g[1][i]==inf)
continue;
int x=find(i);//x为i节点对应连通块的根节点
if(g[1][i]<minedge[x])//minedge初始化为无穷大
{
minedge[x]=g[1][i];//minedge存放终点到连通块的最小值大小
linkpoint[x]=i;//linkpoint存储终点到连通块最小值边对应的点
}
}
for(int i=1;i<=cnt;i++)//第二步,连接终点与各个连通块
{
if(linkpoint[i])// ||minedge[i]
{
int x=linkpoint[i];
ans+=g[1][x];
tree[1][x]=1;//tree[x][y]为1表示在当前连通块中,否则不在
tree[x][1]=1;
m++;//连通块个数
}
}
for(int i=m+1;i<=k;i++)//第三步第四部
{
memset(dp,-1,sizeof(dp));
dp[1].z=-inf;
for(int j=2;j<=cnt;j++)
if(tree[1][j])
dp[j].z=-inf;
dfs(1,-1);//通过从起点dfs找到终点到各个节点路径上的权值最大的边,并将起点存在dp[i].x,终点存在dp[i].y,边权dp[i].z
//对于除了终点的所有节点j,如果g[1][j]-dp[j].z<0说明可以通过将e(dp[j].x,dp[j].y)替换为e(1,j)使得生成树权值和变小
int idx,minmum=inf;
for(int j=2;j<=cnt;j++)
{
if(minmum>g[1][j]-dp[j].z)
{
minmum=g[1][j]-dp[j].z;
idx=j;
}
}
if(minmum>0)//无法找到对应的j使得g[1][j]-dp[j].z<0,退出循环
break;
tree[idx][1]=tree[1][idx]=1;//将替换的边加入当前生成树中
tree[dp[idx].x][dp[idx].y]=tree[dp[idx].y][dp[idx].x]=1;
ans+=minmum;//更新答案
}
}
void init()//初始化
{
cnt=1;
tot=0;
ans=0;
m=0;
mp["Park"]=1;
memset(g,inf,sizeof(g));
memset(tree,0,sizeof(tree));
memset(minedge,inf,sizeof(minedge));
memset(linkpoint,0,sizeof(linkpoint));
for(int i=1;i<=maxn;i++)
fa[i]=i;
}
int main()
{
cin>>n;
string s1,s2;
int t;
init();
for(int i=1;i<=n;i++)
{
cin>>s1>>s2>>t;
if(!mp[s1])
mp[s1]=++cnt;
if(!mp[s2])
mp[s2]=++cnt;
int u=mp[s1];
int v=mp[s2];
edge[++tot].x=u;
edge[tot].y=v;
edge[tot].z=t;
g[u][v]=g[v][u]=min(g[u][v],t);
}
cin>>k;
Kruskal();
parktoother();
cout<<"Total miles driven: "<<ans<<endl;
return 0;
}