挑战程序竞赛系列(45):4.1Polya 计数定理(1)

2019-05-26 09:38:46 浏览数 (1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434700

挑战程序竞赛系列(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)m^{gcd(k, n)}

此时答案为:

ans=1n∑k=0n−1mgcd(k,n)

ans = frac{1}{n} sum_{k = 0}^{n - 1} m^{gcd(k, n)}

我们可以用枚举的方法算出这公式的答案。

POJ 1286: Necklace of Beads

好吧,不做一题果然是领悟不了polya计数的精髓所在,唉。其实polya重在找置换群,像此题的置换群有俩,一个是旋转的置换群,另一个则是翻转的置换群,而找寻完所有置换群后,就能感受polya置换的强大了。

其中旋转和《挑战》P302是一个道理,那么翻转的置换群如何计数?

非常重要的一点,每个置换,找寻的是在发生置换时,状态不变的个数。比如在找翻转时,我们关注点在于哪些状态在翻转前后是没有变化的!!!

所以分奇偶讨论,如果n为奇数:

说明可以构成n个对称轴,那么就有n个置换群,而每个置换群不变的状态个数都一致,取出对称轴上的点有三种染色方式,还有其余(n−1)/2(n - 1) / 2个点均可以染三种颜色,所以不变的状态数为:

3×3n−12

3 times 3^{frac{n - 1}{2}}

同理如果n为偶数,我们能得到两种对称方式,分别为n/2个置换群,所以总数也为n个置换群。

参考自hankcs

偶数时,如题图左,对称轴可能是两个珠子的连线,一共 n / 2条。选定对称轴后,对称轴上的两个珠子构成两个循环,其他n-2个珠子分别以对称轴对称构成(n-2)/2个循环;对称轴还可能是两个珠子的中点和圆心的连线,所有珠子两两对称,构成n / 2 个循环。

代码如下:(注意 n = 0的特判,否则RE)

代码语言:javascript复制
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出现的次数}

ans = frac{1}{n}sum_{d | n}m^d times {d出现的次数}

ok,d出现的次数是什么呢?假设gcd(k, n) = d,且d一定是n的一个约束,k满足[0, n - 1),所以满足k = dt的t的个数即为d出现的次数,于是有:

t∈[0,n/d)

t in [0, n / d)

嗯哼,那么t和n/d满足什么关系呢?d = gcd(k, n ) = d * gcd(k / d, n / d),所以一旦除了最大公约数,那么gcd(t, n / d) = 1,该定义实际就是欧拉函数的值ϕ(n/d)phi(n / d)

所以上式即为:

ans=1n∑d|nmd×ϕ(n/d)

ans = frac{1}{n}sum_{d | n}m^d times phi(n / d)

具体就可以写了,如下:

代码语言:javascript复制
/******************约数枚举***********************/
    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)phi(d)求的就是小于d的素数的乘积,而d的质因数一定是n的质因数,代码如下:

代码语言:javascript复制
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,代码如下:

代码语言:javascript复制
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);
        }
    }
}

0 人点赞