挑战程序竞赛系列(45):4.1Polya 计数定理(1)
2017-09-01 18:09
423 查看
挑战程序竞赛系列(45):4.1Polya 计数定理(1)
详细代码可以fork下Github上leetcode项目,不定期更新。练习题如下:
POJ 1286: Necklace of Beads
POJ 2409: Let it Bead
Polya计数定理
关于Polya的基础知识可以参考两篇博文,写的比较好&&详细。博文1《Polya定理总结 》:
http://endlesscount.blog.163.com/blog/static/82119787201221324524202/
博文2《Polya计数》:
http://blog.csdn.net/y20070316/article/details/50573488
第二篇都是公式扫盲,喜欢公式的可以看看,本人试图理解公式背后的含义,无奈想象力不够,只能理解公式的正确性,却无法想象它的一种计数过程,难道这东西真的只能从抽象的角度证明么?
不过看完《挑战》还是有些心得,来分享一下。本人组合数学就高中学了那么一点知识,看完这种计数方法,简直被作者的智商所折服,唉,无奈高中太不爱学习,现在老大徒伤悲。
在组合问题中,往往让我们求一些状态相异的个数,而Polya计数的应用背景则是为了解决在很多状态中出现本质相同的情况,那么如何取出这些本质相同的状态?
这让我想到了二项式定理,其中有一个握手问题,房间内有n个人,问两两握手总共需要握手几次,你可以采用公式C(n,2)来做,但个人不推荐拿公式套。
其实,他的思路是先假设当前有n个人,每个人能够除自身外,可以握手n-1次,那么总共就用n * (n - 1)次,那么问题来了,真的有这么多?
NO,我们都知道,我跟你握是以我的视角,这和他跟我握本质上是一样的,所以我们在原来的基础上还需除以2,所以有答案:n * (n - 1) / 2
这就是Polya计数,因为上述例子太简单了,所以我们忽略了它背后的思想,它的核心思想是:
先把所有方案重复计算了相同的次数,然后再把结果除以重复的次数,像这样的计数方法,被称作Polya计数定理。
那么对于一个组合问题,比如《挑战》P300的格子染色问题,我们以同样的视角来计算一种包含重复状态的方法,最后再除以一个系数即可。
这里需要专业知识的补充了,涉及的概念有置换群,不动置换类和等价类,具体可以参考:
国家集训队《Pólya原理及其应用》-符文杰
里面的证明和定理都写的很详细,起码这种方法在理论上得到了证明,至于背后更深的关系,本人道行还潜,不去讲了。
我们再来看看《挑战》P302上的内容:
它是Polya的具体实现方法,这里讲讲我对t = GCD(K, N)新的认识吧,可以把N当作一个由(0, n - 1)的结点围成的圈,K表示每隔K步,状态相同的结点,那么如果GCD的值大于1,说明就有t条轨迹,且假设从某个状态i开始,不断前进K步的过程中,能够回到最初的位置i。而当GCD = 1时,只能说明没有一条路径能够让自己走k步后回到最初位置。
既然有了一个圈的轨迹数,我们就能枚举每个置换没有发生变化的状态了,为mgcd(k,n)
此时答案为:
ans=1n∑k=0n−1mgcd(k,n)
我们可以用枚举的方法算出这公式的答案。
POJ 1286: Necklace of Beads
好吧,不做一题果然是领悟不了polya计数的精髓所在,唉。其实polya重在找置换群,像此题的置换群有俩,一个是旋转的置换群,另一个则是翻转的置换群,而找寻完所有置换群后,就能感受polya置换的强大了。其中旋转和《挑战》P302是一个道理,那么翻转的置换群如何计数?
非常重要的一点,每个置换,找寻的是在发生置换时,状态不变的个数。比如在找翻转时,我们关注点在于哪些状态在翻转前后是没有变化的!!!
所以分奇偶讨论,如果n为奇数:
说明可以构成n个对称轴,那么就有n个置换群,而每个置换群不变的状态个数都一致,取出对称轴上的点有三种染色方式,还有其余(n−1)/2个点均可以染三种颜色,所以不变的状态数为:
3×3n−12
同理如果n为偶数,我们能得到两种对称方式,分别为n/2个置换群,所以总数也为n个置换群。
参考自hankcs
偶数时,如题图左,对称轴可能是两个珠子的连线,一共 n / 2条。选定对称轴后,对称轴上的两个珠子构成两个循环,其他n-2个珠子分别以对称轴对称构成(n-2)/2个循环;对称轴还可能是两个珠子的中点和圆心的连线,所有珠子两两对称,构成n / 2 个循环。
代码如下:(注意 n = 0的特判,否则RE)
import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.StringTokenizer; public class Main { String INPUT = "./data/judge/201709/P1286.txt"; public static void main(String[] args) throws IOException { new Main().run(); } public int gcd(int a, int b){ return b == 0 ? a : gcd(b, a % b); } public long pow(int a, int b){ long res = 1; for (; b > 0; b >>= 1, a = a * a){ if ((b & 1) != 0){ res *= a; } } return res; } static final int m = 3; void solve() { while (true){ int n = ni(); if (n == -1) break; if (n == 0){ out.println(0); continue; } long count = 0; for (int i = 0; i < n; ++i){ count += pow(m, gcd(i, n)); } if ((n & 1) != 0){ //奇数 count += n * pow(m, (n + 1) / 2); } else{ count += n / 2 * (pow (m, n / 2 + 1) + pow(m, n / 2)); } out.println(count / (2 * n)); } } FastScanner in; PrintWriter out; void run() throws IOException { boolean oj; try { oj = ! System.getProperty("user.dir").equals("F:\\java_workspace\\leetcode"); } catch (Exception e) { oj = System.getProperty("ONLINE_JUDGE") != null; } InputStream is = oj ? System.in : new FileInputStream(new File(INPUT)); in = new FastScanner(is); out = new PrintWriter(System.out); long s = System.currentTimeMillis(); solve(); out.flush(); if (!oj){ System.out.println("[" + (System.currentTimeMillis() - s) + "ms]"); } } public boolean more(){ return in.hasNext(); } public int ni(){ return in.nextInt(); } public long nl(){ return in.nextLong(); } public double nd(){ return in.nextDouble(); } public String ns(){ return in.nextString(); } public char nc(){ return in.nextChar(); } class FastScanner { BufferedReader br; StringTokenizer st; boolean hasNext; public FastScanner(InputStream is) throws IOException { br = new BufferedReader(new InputStreamReader(is)); hasNext = true; } public String nextToken() { while (st == null || !st.hasMoreTokens()) { try { st = new StringTokenizer(br.readLine()); } catch (Exception e) { hasNext = false; return "##"; } } return st.nextToken(); } String next = null; public boolean hasNext(){ next = nextToken(); return hasNext; } public int nextInt() { if (next == null){ hasNext(); } String more = next; next = null; return Integer.parseInt(more); } public long nextLong() { if (next == null){ hasNext(); } String more = next; next = null; return Long.parseLong(more); } public double nextDouble() { if (next == null){ hasNext(); } String more = next; next = null; return Double.parseDouble(more); } public String nextString(){ if (next == null){ hasNext(); } String more = next; next = null; return more; } public char nextChar(){ if (next == null){ hasNext(); } String more = next; next = null; return more.charAt(0); } } }
此做法是暴力枚举,枚举发生在gcd身上,因为gcd是有限个不同的值,我们完全可以让它【并发】执行来降低时间复杂度。
具体参考《挑战》P303,简单说说思想。
观察式子gcd(k, n)发现,所有的k都在[0, n - 1)之间,所以gcd(k, n)求出的解一定都是n的约束,而不可能出现其他解,这样我们只需要知道每个约束d的个数就能把上述式子改写为:
ans=1n∑d|nmd×{d出现的次数}
ok,d出现的次数是什么呢?假设gcd(k, n) = d,且d一定是n的一个约束,k满足[0, n - 1),所以满足k = dt的t的个数即为d出现的次数,于是有:
t∈[0,n/d)
嗯哼,那么t和n/d满足什么关系呢?d = gcd(k, n ) = d * gcd(k / d, n / d),所以一旦除了最大公约数,那么gcd(t, n / d) = 1,该定义实际就是欧拉函数的值ϕ(n/d)
所以上式即为:
ans=1n∑d|nmd×ϕ(n/d)
具体就可以写了,如下:
/******************约数枚举***********************/ public Set<Integer> prime_factors(int n){ Set<Integer> ans = new HashSet<Integer>(); for (int i = 1; i <= n / i; ++i){ if (n % i == 0){ ans.add(i); ans.add(n / i); } } return ans; } /*******************计算欧拉函数*******************/ public int phi(int n){ int res = n; for (int i = 2; i <= n / i; ++i){ if (n % i == 0){ res = res / i * (i - 1); for (; n % i == 0; n /= i); } } if (n != 1){ res = res / n * (n - 1); } return res; } void solve() { while (true){ int n = ni(); if (n == -1) break; if (n == 0){ out.println(0); continue; } Set<Integer> factors = prime_factors(n); long count = 0; for (int d : factors){ count += phi(n / d) * pow(m, d); } if ((n & 1) != 0){ //奇数 count += n * pow(m, (n + 1) / 2); } else{ count += n / 2 * (pow (m, n / 2 + 1) + pow(m, n / 2)); } out.println(count / (2 * n)); } }
当然,不要忘记了欧拉函数的最初定义,ϕ(d)求的就是小于d的素数的乘积,而d的质因数一定是n的质因数,代码如下:
import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.HashSet; import java.util.Set; import java.util.StringTokenizer; public class Main{ String INPUT = "./data/judge/201709/P1286.txt"; public static void main(String[] args) throws IOException { new Main().run(); } public int gcd(int a, int b){ return b == 0 ? a : gcd(b, a % b); } public long pow(int a, int b){ long res = 1; for (; b > 0; b >>= 1, a = a * a){ if ((b & 1) != 0){ res *= a; } } return res; } static final int m = 3; /******************约数枚举***********************/ public Set<Integer> divisor(int n){ Set<Integer> ans = new HashSet<Integer>(); for (int i = 1; i <= n / i; ++i){ if (n % i == 0){ ans.add(i); ans.add(n / i); } } return ans; } public Set<Integer> primes_factor(int n){ Set<Integer> ans = new HashSet<Integer>(); for (int i = 2; i <= n / i; ++i){ if (n % i == 0){ ans.add(i); for (; n % i == 0; n /= i); } } //针对素数的情况,直接返回一个素数 if (n != 1){ ans.add(n); } return ans; } void solve() { while (true){ int n = ni(); if (n == -1) break; if (n == 0){ out.println(0); continue; } Set<Integer> factors = divisor(n); Set<Integer> primes = primes_factor(n); long count = 0; for (int d : factors){ int phi = n / d; for (int p : primes){ if ((n / d) % p == 0) phi = phi / p * (p - 1); } count += phi * pow(m, d); } if ((n & 1) != 0){ //奇数 count += n * pow(m, (n + 1) / 2); } else{ count += n / 2 * (pow (m, n / 2 + 1) + pow(m, n / 2)); } out.println(count / (2 * n)); } } FastScanner in; PrintWriter out; void run() throws IOException { boolean oj; try { oj = ! System.getProperty("user.dir").equals("F:\\java_workspace\\leetcode"); } catch (Exception e) { oj = System.getProperty("ONLINE_JUDGE") != null; } InputStream is = oj ? System.in : new FileInputStream(new File(INPUT)); in = new FastScanner(is); out = new PrintWriter(System.out); long s = System.currentTimeMillis(); solve(); out.flush(); if (!oj){ System.out.println("[" + (System.currentTimeMillis() - s) + "ms]"); } } public boolean more(){ return in.hasNext(); } public int ni(){ return in.nextInt(); } public long nl(){ return in.nextLong(); } public double nd(){ return in.nextDouble(); } public String ns(){ return in.nextString(); } public char nc(){ return in.nextChar(); } class FastScanner { BufferedReader br; StringTokenizer st; boolean hasNext; public FastScanner(InputStream is) throws IOException { br = new BufferedReader(new InputStreamReader(is)); hasNext = true; } public String nextToken() { while (st == null || !st.hasMoreTokens()) { try { st = new StringTokenizer(br.readLine()); } catch (Exception e) { hasNext = false; return "##"; } } return st.nextToken(); } String next = null; public boolean hasNext(){ next = nextToken(); return hasNext; } public int nextInt() { if (next == null){ hasNext(); } String more = next; next = null; return Integer.parseInt(more); } public long nextLong() { if (next == null){ hasNext(); } String more = next; next = null; return Long.parseLong(more); } public double nextDouble() { if (next == null){ hasNext(); } String more = next; next = null; return Double.parseDouble(more); } public String nextString(){ if (next == null){ hasNext(); } String more = next; next = null; return more; } public char nextChar(){ if (next == null){ hasNext(); } String more = next; next = null; return more.charAt(0); } } }
POJ 2409: Let it Bead
和第一题一样,无非现在m变成了输入,稍微改动下即能AC,代码如下:import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.HashSet; import java.util.Set; import java.util.StringTokenizer; public class Main{ String INPUT = "./data/judge/201709/P2409.txt"; public static void main(String[] args) throws IOException { new Main().run(); } public Set<Integer> primes_factor(int n){ Set<Integer> ans = new HashSet<Integer>(); for (int i = 2; i <= n / i; ++i){ if (n % i == 0){ ans.add(i); for (; n % i == 0; n /= i); } } if (n != 1){ ans.add(n); } return ans; } public Set<Integer> divisor(int n){ Set<Integer> ans = new HashSet<Integer>(); for (int i = 1; i <= n / i; ++i){ if (n % i == 0){ ans.add(i); ans.add(n / i); } } return ans; } public long pow(int a, int b){ long res = 1; for (; b > 0; b >>= 1, a = a * a){ if ((b & 1) != 0){ res = res * a; } } return res; } public int gcd(int a, int b){ return b == 0 ? a : gcd(b, a % b); } void solve() { while (true){ int m = ni(); int n = ni(); if (m + n == 0) break; if (n == 0) out.println(0); else{ Set<Integer> factors = divisor(n); Set<Integer> primes = primes_factor(n); long count = 0; for (int d : factors){ int phi = n / d; for (int p : primes){ if ((n / d) % p == 0) phi = phi / p * (p - 1); } count += phi * pow(m, d); } if ((n & 1) != 0){ count += n * pow(m, (n + 1) / 2); } else{ count += n / 2 * (pow(m, n / 2 + 1) + pow(m, n / 2)); } out.println(count / (2 * n)); } } } FastScanner in; PrintWriter out; void run() throws IOException { boolean oj; try { oj = ! System.getProperty("user.dir").equals("F:\\java_workspace\\leetcode"); } catch (Exception e) { oj = System.getProperty("ONLINE_JUDGE") != null; } InputStream is = oj ? System.in : new FileInputStream(new File(INPUT)); in = new FastScanner(is); out = new PrintWriter(System.out); long s = System.currentTimeMillis(); solve(); out.flush(); if (!oj){ System.out.println("[" + (System.currentTimeMillis() - s) + "ms]"); } } public boolean more(){ return in.hasNext(); } public int ni(){ return in.nextInt(); } public long nl(){ return in.nextLong(); } public double nd(){ return in.nextDouble(); } public String ns(){ return in.nextString(); } public char nc(){ return in.nextChar(); } class FastScanner { BufferedReader br; StringTokenizer st; boolean hasNext; public FastScanner(InputStream is) throws IOException { br = new BufferedReader(new InputStreamReader(is)); hasNext = true; } public String nextToken() { while (st == null || !st.hasMoreTokens()) { try { st = new StringTokenizer(br.readLine()); } catch (Exception e) { hasNext = false; return "##"; } } return st.nextToken(); } String next = null; public boolean hasNext(){ next = nextToken(); return hasNext; } public int nextInt() { if (next == null){ hasNext(); } String more = next; next = null; return Integer.parseInt(more); } public long nextLong() { if (next == null){ hasNext(); } String more = next; next = null; return Long.parseLong(more); } public double nextDouble() { if (next == null){ hasNext(); } String more = next; next = null; return Double.parseDouble(more); } public String nextString(){ if (next == null){ hasNext(); } String more = next; next = null; return more; } public char nextChar(){ if (next == null){ hasNext(); } String more = next; next = null; return more.charAt(0); } } }
相关文章推荐
- 挑战程序竞赛系列(46):4.1Polya 计数定理(2)
- 挑战程序竞赛系列(41):4.1中国剩余定理
- 挑战程序竞赛系列(44):4.1计数 欧拉函数
- 挑战程序竞赛系列(47):4.1 计数 二项式取模
- 挑战程序竞赛系列(43):4.1矩阵 高斯消元
- 挑战程序竞赛系列(7):2.1一往直前!贪心法(区间)
- 挑战程序竞赛系列(32):4.5 A*与IDA*
- 挑战程序竞赛系列(35):3.3Binary Indexed Tree
- 挑战程序竞赛系列(69):4.7后缀数组(1)
- 挑战程序竞赛系列(38):4.1模运算的世界(1)
- 挑战程序竞赛系列(39):4.1模运算的世界(2)
- 挑战程序竞赛系列(5):2.1广度优先搜索
- 挑战程序竞赛系列(89):3.6平面扫描(3)
- 挑战程序竞赛系列(20):3.2尺取法
- 挑战程序竞赛系列(31):4.5剪枝
- 挑战程序竞赛系列(92):3.6凸包(3)
- 挑战程序竞赛系列(54):4.4 双端队列(1)
- 挑战程序竞赛系列(19):3.1最小化第k大的值
- 挑战程序竞赛系列(94):3.6凸包(5)
- 挑战程序竞赛系列(40):4.1模运算的世界(3)