最小树形图+朱刘算法
大题上完整的朱、刘算法是由四个大步骤组成的:
1、求最短弧集合E
2、判断集合E中有没有有向环,如果有转步骤3,否则转4
3、收缩点,把有向环收缩成一个点,并且对图重新构建,包括边权值的改变和点的处理,之后再转步骤1。
4、展开收缩点,求得最小树形图。
因为我们ACM一般情况下都是在考察队最小树型图的权值问题,所以一般省略步骤4,对于其环的权值和在中间处理过程中就可以处理完毕。所以我们这里就不多讨论第四个点了。
我们分步处理、
1、首先我们先求最短弧集合E,对于当前图如果有n个点(一个有向环的收缩点算作一个点),我们就要选出n-1个点,确定其入边的最短边,由其组成的一个集合我们就叫做最短弧集合E,如果我们枚举到某一个点的时候,它没有入边,那么说明不存在最小树形图,所以这个时候算法结束,回到主函数。
代码实现:
for(int i=1; i<=n; i++)if(i!=u&&!flag[i])//u作为图的根节点,flag【i】为1的情况就是表示这个点在某个有向环里边,并且他不是这个有向环的代表点(缩点)
{
w[i][i]=INF, pre[i] = i;//首先让当前点的前驱点为自己。
for(int j=1; j<=n; j++)if(!flag[j] && w[j][i]<w[pre[i]][i])//枚举i的前驱点(从j能够到i的点),并且求其最短边,加入集合E中
{
pre[i] = j;//并且标记当前点的前驱点为j
}
if(pre[i]==i)return -1;//如果当前枚举到的点i没有入边,那么就不存在最小树形图(因为一颗树是要所有节点都是连通的啊)
}
2、然后我们对集合E中的边进行判断,判断是否有有向环。刚刚的代码实现里边有一个前驱节点的存储,所以在这个部分,我们直接一直向前枚举前驱点即可,如果枚举的前驱点最终能够枚举到根节点,那么这一部分就不存在有向环,否则就存在,对于每一个点都进行向前枚举即可。
int i;
for(i=1; i<=n; i++)
{
if(i!=u&&!flag[i])
{
int j=i, cnt=0;
while(j!=u && pre[j]!=i && cnt<=n) j=pre[j], ++cnt;//对于每个节点都找前驱节点,看看能否成环。
if(j==u || cnt>n) continue; //最后能找到起点(根)或者是走过的点已经超过了n个,表示没有有向环
break;//表示有有向环
}
}
3、如果有有向环呢,我们需要对有向环进行缩点,既然我们是枚举到节点i的时候发现有有向环,我们不妨把有向环里边的点都收缩成点i。对于收缩完之后会形成一个新的图,图的变化规律是这样的:
上图变换成语言描述:如果点u在环内,如果点k在环外,并且从k到u有一条边map【u】【v】=w,并且在环内还有一点i,使得map【i】【k】=w2,辣么map【k】【收缩点】=w-w2;
基于贪心思想,对于环的收缩点i和另外一点k(也在环内),对于环外一点j,如果map【k】【j】 <map【i】【j】,辣么map【i】【j】=map【k】【j】,因为是有向图,入收缩点的边要这样处理,出收缩点的边也要这样处理,对于刚刚三个步骤:收缩点,收缩点处理新图的边权值,以及基于贪心思想的最终处理点i的出边入边权值,后两者我们可以合并成一个操作,其分别的代码实现:
3、收缩点:
int j=i;
memset(vis, 0, sizeof(vis));
do{
ans += w[pre[j]][j], j=pre[j], vis[j]=flag[j]=true;//对环内的点标记,并且直接对环的权值进行加和记录,在最后找到最小树形图之后就不用展开收缩点了
}while(j!=i);
flag[i] = false; // 环缩成了点i,点i仍然存在
4、处理收缩点后形成的图:
for(int k=1; k<=n; ++k)if(vis[k]) // 在环中点点,刚刚在收缩点的时候,已经把在环中的点进行标记了。
{
for(int j=1; j<=n; j++)if(!vis[j]) // 不在环中的点
{
if(w[i][j] > w[k][j]) w[i][j] = w[k][j];
if(w[j][k]<INF && w[j][k]-w[pre[k]][k] < w[j][i])
w[j][i] = w[j][k] - w[pre[k]][k];
}
}
处理完4之后,我们就回到步骤1,继续找最小弧集E,最后找到了一个没有环的最小弧集E之后,对于没有弧的集合E中的所有边(包括能将收缩点展开的边)就是我们要求的最小树形图的边集。
因为我们ACM一般求的都是最小树形图的权值,所以我们一般不需要展开收缩点,在处理环的时候,直接将其边权值记录下来就好,当找到一个没有环的集合E的时候,对其中的最后边权值进行加和即可,对于最后这部分的加权,代码实现:
for(int k=1; k<=n; ++k)if(vis[k]) // 在环中点点,刚刚在收缩点的时候,已经把在环中的点进行标记了。
{
for(int j=1 ; j<=n; j++)if(!vis[j]) // 不在环中的点
{
if(w[i][j] > w[k][j]) w[i][j] = w[k][j];
if(w[j][k]<INF && w[j][k]-w[pre[k]][k] < w[j][i])
w[j][i] = w[j][k] - w[pre[k]][k];
}
}
完整的朱、刘算法代码实现(没有展开收缩点的):
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define INF 0x3f3f3f3f
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int maxN = 1005;
int N, M;
ll sum;
struct Eddge //存边
{
int u, v;
ll val;
Eddge(int a=0, int b=0, ll c=0):u(a), v(b), val(c) {}
}edge[maxN*maxN];
int pre[maxN], id[maxN], vis[maxN], pos;
ll in[maxN]; //最小入边权,pre[]为其前面的点(该边的起点)
ll Dir_MST(int root, int V, int E) //root是此时的根节点,我们最初的时候将0(万能节点作为根节点进入),V是点的个数(包括之后要收缩之后点的剩余个数),E是边的条数(不会改变)
{
ll ans = 0;
while(true) //如果还是可行的话
{
for(int i=0; i<V; i++) in[i] = INF; //给予每个点进行初始化
/* (1)、最短弧集合E0 */
for(int i=1; i<=E; i++) //通过这么多条单向边,确定的是每个点的指向边的最小权值
{
int u = edge[i].u, v = edge[i].v;
if(edge[i].val < in[v] && u!=v) //顶点v有更小的入边,记录下来 更新操作,u!=v是为了确保缩点之后,我们的环将会变成点的形式
{
pre[v] = u; //节点u指向v
in[v] = edge[i].val; //最小入边
if(u == root) pos = i; //这个点就是实际的起点
}
}
/* (2)、检查E0 */
for(int i=0; i<V; i++) //判断是否存在最小树形图
{
if(i == root) continue; //是根节点,不管
if(in[i] == INF) return -1; //除了根节点以外,有点没有入边,则根本无法抵达它,说明是独立的点,一定不能构成树形图
}
/* (3)、收缩图中的有向环 */
int cnt = 0; //接下来要去求环,用以记录环的个数 找环开始!
memset(id, -1, sizeof(id));
memset(vis, -1, sizeof(vis));
in[root] = 0;
for(int i=0; i<V; i++) //标记每个环
{
ans += in[i]; //加入每个点的入边(既然是最小入边,所以肯定符合最小树形图的思想)
int v = i; //v一开始先从第i个节点进去
while(vis[v] != i && id[v] == -1 && v != root) //退出的条件有“形成了一个环,即vis回归”、“到了一个环,此时就不要管了,因为那边已经建好环了”、“到了根节点,就是条链,不用管了”
{
vis[v] = i;
v = pre[v];
}
if(v != root && id[v] == -1) //如果v是root就说明是返回到了根节点,是条链,没环;又或者,它已经是进入了对应环的编号了,不需要再跑一趟了
{
for(int u=pre[v]; u!=v; u=pre[u]) //跑这一圈的环
{
id[u] = cnt; //标记点u是第几个环
}
id[v] = cnt++; //如果再遇到,就是下个点了
}
}
if(cnt == 0) return ans; //无环的情况,就说明已经取到了最优解,直接返回,或者说是环已经收缩到没有环的情况了
for(int i=0; i<V; i++) if(id[i] == -1) id[i] = cnt++; //这些点是环外的点,是链上的点,单独再给他们赋值
for(int i=1; i<=E; i++) //准备开始建立新图 缩点,重新标记
{
int u = edge[i].u, v = edge[i].v;
edge[i].u = id[u]; edge[i].v = id[v]; //建立新图,以新的点进入
if(id[u] != id[v]) edge[i].val -= in[v]; //为了不改变原来的式子,使得展开后还是原来的式子
}
V = cnt; //之后的点的数目
root = id[root]; //新的根节点的序号,因为id[]的改变,所以根节点的序号也改变了
}
return ans;
}
int main()
{
while(scanf("%d%d", &N, &M)!=EOF)
{
sum = 0;
for(int i=1; i<=M; i++)
{
scanf("%d%d%lld", &edge[i].u, &edge[i].v, &edge[i].val);
edge[i].u++; edge[i].v++; //把‘0’号节点空出来,用以做万能节点,留作之后用
sum += edge[i].val;
}
sum++; //一定要把sum给扩大,这就意味着,除去万能节点以外的点锁构成的图的权值和得在(sum-1)之内(包含)
for(int i=M+1; i<=M+N; i++) //这就是万能节点了,就是从0这号万能节点有通往所有其他节点的路,而我们最后的最小树形图就是从这个万能节点出发所能到达的整幅图
{
edge[i] = Eddge(0, i-M, sum); //对于所有的N个其他节点都要建有向边
} //此时N+1为总的节点数目,M+N为总的边数
ll ans = Dir_MST(0, N + 1, M+N); //ans代表以超级节点0为根的最小树形图的总权值
if(ans == -1 || ans - sum >= sum) printf("impossible\n"); //从万能节点的出度只能是1,所以最后的和必须是小于sum的,而万能节点的出度就由“ans - sum >= sum”保证
else printf("%lld %d\n", ans - sum, pos - M - 1); //pos-M得到的是1~N的情况,所以“-1”的目的就在于这里
printf("\n");
}
return 0;
}