1、题目
给定n个互不相同的顶点,求它们可以构成的不相同的无向连通图数量
http://poj.org/problem?id=1737
2、算法思路
引文思路描述对我来说有点抽象,这里尝试再说细一些(引文列表见本文文末)
思路一:直接求连通图的方案数(本文代码)
递推公式:F(n) = Sum{ F(k)∗F(n-k)∗C(n-2,k-1)∗(2^k -1) | 1<=k<n }
(1)将整个连通图划分为两部分
首先,考虑把n个节点构成的整个连通图分为A和B两块,若连通块A包含k个节点(k个节点含节点2,k的取值从1至n-1),则另外一个连通块B包含n-k个节点(n-k个节点含节点1)
A、B两部分各自的连通图方案数分别为F(A)和F(B),则组合起来的方案数有:
F(k)*F(n-k)
连通块A的取法:A部分除去节点2的k-1个节点从所有n个节点(除去节点1和节点2)中选取,因此有:
C(n-2,k-1)
TODO:这里为什么是C(n-2,k-1),而不是C(n,k)?
(2)将两个部分连接起来
接着,由于整体上要求由n个节点构成的图是连通的,因此要设法将上述的连通块A和连通块B连接起来。如图,从节点1到A部分的k个节点的k个连接一共有2^k种排列组合的连接方式,需要排除全不连接的情况,因此满足条件的有:
2^k-1种
综合起来,得到思路一的递推公式:
F(n) = Sum{ F(k)∗F(n-k)∗C(n-2,k-1)∗(2^k -1) | 1<=k<n }
思路二:将总的方案数减掉所有不连通图的方案数
(1)总的方案数
n个节点的全连通图(即任意两点间都有边)的连接数为C(n,2);考虑每个边的存在和不存在2种情况,因此,总的方案数为:
2^(C(n,2))
(2)非连通图的方案数
将整个节点集合考虑为2个部分:包括节点1的连通部分A(含k个节点);以及不包括节点1的其它部分B(含n-k个节点)
a.连通部分A的考虑
首先,划分出包括k个节点连通部分A:A中除去点1,其余k-1个节点从剩余n个节点中取,取法数为:
C(n-1,k-1)
A为连通区域,连通图方案数为:
F(k)
因此,连通区域A的方案数为:
C(n-1,k-1)∗F(k)
b.其它部分B的考虑
B中的n-k的各点间任意连边即可,方案数为:
2^(C(n-k,2))
综合起来,得到思路二的递推公式:
F(n)= 2^(C(n,2))-Sum{ C(n-1,k-1) * F(k) * 2^(C(n-k,2)) | 0<=k<n }
4、代码实现
(Java代码)
import java.util.Scanner;
import java.math.BigInteger;
public class Main {
private static BigInteger[] _f = new BigInteger[100];
private static BigInteger E(int base, int n) {
if (n == 0) {
return BigInteger.valueOf(1);
}
BigInteger result = BigInteger.valueOf(base);
for (int i=1; i<n; i++) {
result = result.multiply(BigInteger.valueOf(base));
}
return result;
}
private static BigInteger factorial(int n) {
if (n == 0) {
return BigInteger.valueOf(1);
}
BigInteger result = BigInteger.valueOf(1);
for (int i=1; i<=n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
private static BigInteger C(int n, int m) {
return factorial(n).divide(factorial(m)).divide(factorial(n-m));
}
/* 思路一:直接求连通图方案数
* 公式:F(n) = Sum{ F(k)∗F(n-k)∗C(n-2,k-1)∗(2^k -1) | 1<=k<n }
*/
private static BigInteger F(int n) {
if (_f[n] != null) return _f[n];
if (n<=2) {
return BigInteger.valueOf(1);
}
BigInteger res = BigInteger.valueOf(0);
for (int i=1; i<n; i++) {
BigInteger m1 = F(i).multiply(F(n-i));
BigInteger m2 = C(n-2, i-1);
BigInteger m3 = E(2, i).subtract(BigInteger.valueOf(1));
res = res.add(m1.multiply(m2).multiply(m3));
}
_f[n] = res;
return res;
}
/* 思路二:总方案数-非联通图方案数
* 公式:F(n)= 2^(C(n,2))-Sum{ C(n-1,k-1) * F(k) * 2^(C(n-k,2)) | 0<=k<n }
*/
// private static BigInteger F(int n) {
// if (_f[n] != null) return _f[n];
// BigInteger all = E(2,C(n,2).intValue());
// BigInteger except = BigInteger.valueOf(0);
// for (int i=1; i<n; i++) {
// BigInteger m1 = C(n-1, i-1);
// BigInteger m2 = F(i);
// BigInteger m3 = E(2, C(n-i,2).intValue());
// except = except.add(m1.multiply(m2).multiply(m3));
// }
// BigInteger res = all.subtract(except);
// _f[n] = res;
// return res;
// }
public static void main(String arg[]){
Scanner s = new Scanner(System.in);
while(s.hasNextInt()) {
int n = s.nextInt();
if (n == 0) break;
System.out.println(F(n));
}
return;
}
}
4、最后说几个点
(1)引文说明:网上关于这题有很多检索结果,但是不少还是存在谬误(公式错或公式和代码对不上等等);下面几篇文章就正、反两种思路给出了比较详细的分析,也给出了部分代码:https://www.cnblogs.com/longdouhzt/archive/2012/03/05/2380994.html(思路1公式有问题,思路2正确)
https://blog.csdn.net/wu_tongtong/article/details/79569016(思路1公式正确,但是没给代码)
(2)大整数计算:下文Java代码直接使用BigInteger(ref:https://blog.csdn.net/baidu_41560343/article/details/89971950)
*** 此外:下文描述了根据“同余定理”的模1000000007技巧(本文代码未使用该技巧)
https://blog.csdn.net/weixin_41754415/article/details/88998826
(3)计算效率问题:下文Java代码通过对主计算函数f()加入一片“缓存”(数组_f[])来存储f的计算结果的方式避免重复计算。其实类似的思路也可以用在阶乘计算函数factorial()以及幂函数计算函数E()都可以用类似的方式来处理。但是由于在poj已经AC了(213ms),就没有做进一步处理了
(4)OJ的Java代码“套路”(输入、输出的处理等):https://blog.csdn.net/qq_38174756/article/details/83537860
(5)幂指数函数计算有个小“坑”,一定要做幂等于0的返回1的分支… 基础知识…
“送”一张图
横坐标是节点数量,纵坐标是n个节点构成的所有图中连通图的占比,看上去,n为两位数时,各种情形中99%以上都是连通图了