BZOJ1016 [JSOI2008]最小生成树计数

Description

  现在给出了一个简单无向加权图。你不满足于求出这个图的最小生成树,而希望知道这个图中有多少个不同的
最小生成树。(如果两颗最小生成树中至少有一条边不同,则这两个最小生成树就是不同的)。由于不同的最小生
成树可能很多,所以你只需要输出方案数对31011的模就可以了。

Input

  第一行包含两个数,n和m,其中1<=n<=100; 1<=m<=1000; 表示该无向图的节点数和边数。每个节点用1~n的整
数编号。接下来的m行,每行包含两个整数:a, b, c,表示节点a, b之间的边的权值为c,其中1<=c<=1,000,000,0
00。数据保证不会出现自回边和重边。注意:具有相同权值的边不会超过10条。

Output

  输出不同的最小生成树有多少个。你只需要输出数量对31011的模就可以了。

Sample Input

4 6
1 2 1
1 3 1
1 4 1
2 3 2
2 4 1
3 4 1

Sample Output

8

HINT
Source

分析:

首先需要一个结论,对于一个图的不同最小生成树,每种方案所包含的每种权值的边的数量一定一致。

换句话说,把每种方案包含的所有边的边权都写下来,写出来的序列一定都一样。

证明:

考虑kruskal的过程,开始时,每个点单独构成一个集合。

首先只考虑权值最小的边,将它们全部添加进图中(记作步骤1)。

那么现在的图有若干部分是联通的了。这就是添加完最短边后,能够形成的最多的联通分量的数目,注意,根据Kruskal,也是这种权值的边添加完成之后的联通分量数目(贪心原则)

现在我们得出一个结论,添加完最小权值的边后,最终形成的联通分量是一定的,且集合的划分情况一定相同。

那么真正添加的边数也是相同的。因为每添加一条边集合的数目便减少1,不可能出现添加一条边集合数目不变或减少2的情况。

那么权值第二小的边呢?我们将之间得到的集合每个集合都缩为一个点,那么权值第二小的边就变成了当前权值最小的边,也有上述的结论。

因此每个阶段,添加的边数都是相同的。我们以权值划分阶段,那么也就意味着某种权值的边的数目是完全相同的。

根据上面这个结论,我们只需要用Kruskal求一遍最小生成树,统计每种权值的边出现的次数。

然后dfs对每一种权值的边有多少种选边方法即可

另外,可以看一下周冬的OI集训队论文《生成树的计数及其应用》,里面有更厉害的解法

代码:

  1 #include<cstdio>
  2 #include<cstring>
  3 #include<vector>
  4 #include<algorithm>
  5 using namespace std;
  6 const int mod=31011;
  7 struct edge
  8 {
  9     int x,y,l;
 10     bool operator < (const edge &e) const
 11     {
 12         return l<e.l;
 13     }
 14 }g[1010];
 15 vector<edge> v[1010];
 16 int cnt[1010],f[110],tot;
 17 int find(int x)
 18 {
 19     if (x==f[x]) return x;
 20     f[x]=find(f[x]);
 21     return f[x];
 22 }
 23 int find2(int x)
 24 {
 25     if (x==f[x]) return x;
 26     return find2(f[x]);
 27 }
 28 int dfs(int k,int p,int now)
 29 {
 30     if (p==v[k].size())
 31       return now==cnt[k];
 32     int ret=0,x,y,z;
 33     if (now<cnt[k])
 34     {
 35         x=find2(v[k][p].x);
 36         y=find2(v[k][p].y);
 37         if (x!=y)
 38         {
 39             f[x]=y;
 40             ret+=dfs(k,p+1,now+1);
 41             f[x]=x;
 42         }
 43     }
 44     if (now+v[k].size()-p-1>=cnt[k]) ret+=dfs(k,p+1,now);
 45     return ret;
 46 }
 47 int main()
 48 {
 49     int i,j,k,m,n,p,q,x,y,z,now=0,ans;
 50     scanf("%d%d",&n,&m);
 51     for (i=1;i<=m;i++)
 52       scanf("%d%d%d",&g[i].x,&g[i].y,&g[i].l);
 53     sort(g+1,g+m+1);
 54     for (i=1;i<=n;i++)
 55       f[i]=i;
 56     for (i=1;i<=m;i++)
 57     {
 58         if (g[i].l>g[i-1].l) tot++;
 59         v[tot].push_back(g[i]);
 60         x=find(g[i].x);
 61         y=find(g[i].y);
 62         if (x!=y)
 63         {
 64             cnt[tot]++;
 65             f[x]=y;
 66             now++;
 67         }
 68     }
 69     if (now<n-1)
 70     {
 71         printf("0\n");
 72         return 0;
 73     }
 74     ans=1;
 75     for (i=1;i<=n;i++)
 76       f[i]=i;
 77     for (i=1;i<=tot;i++)
 78     {
 79         ans=(ans*dfs(i,0,0))%mod;
 80         for (j=0;j<v[i].size();j++)
 81         {
 82             x=find(v[i][j].x);
 83             y=find(v[i][j].y);
 84             f[x]=y;
 85         }
 86     }
 87     printf("%d\n",ans);
 88 }

转载于:https://www.cnblogs.com/liuzhanshan/p/6874690.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输格式 输一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求来,然后将其看作是一个有权值的,问题就转化为了在这个中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值