染色问题 —— 扇形涂色


  • 题目描述:
  • 方法一:公式推导法
    • 代码实现
  • 方法二:递归法
    • 法2-1
    • 法2-2
    • 代码实现
    • 算法复杂度分析及改进
  • 总结


综述:
  做了作业帮的题目,感觉总体不是很难,题目很常规,遇到了这个扇形染色问题,顺势做一个总结和分析。精华!!!如遇上跪求读完,没有收获你砍我!!!

题目描述:

  一个圆分成N个扇形,用种颜色上色,要求相邻两个颜色不同,求有多少种不同的方法。
输入描述:
输入切分成的扇形数量(0< n <20)
输出描述:
输出一个整数,表示上色方法数

示例1:
输入:3
输出:24

题目分析
  首先这道题目是将问题简化了,是将一个(n,m)的问题简化为了<20的n4种颜色。n 是分成的块数,m是颜色的个数。


染色问题 —— 扇形涂色_第1张图片
图1. 扇形涂色 n=块数,m=颜色

方法一:公式推导法

  这个办法是推导出求解公式,直接代入公式进行计算就可以。

首先,设A(n)为最后的结果。那么
对第 A1 块扇形,有m种画法,
对第 A2 块扇形,有m-1种画法,
对第 A3 块扇形,有m-1种画法,
(因为只要求相邻的两块颜色不同,所以,只需要看前边的一块就可以了)
对第 A4 块扇形,有m-1种画法,
…………
…………
对第 An 块扇形,有m-1种画法,

既为 m(m1)n1 m ∗ ( m − 1 ) n − 1
但是这时候要分成两类:

  1. An A n A1 A 1 不同色;
  2. An A n A1 A 1 同色。

当颜色不同时符合题目要求,没有影响,
  而当两块相同时,可以将两块看作是一块,这个时候染色方法总数就是 A(n1) A ( n − 1 ) ,这里可能会有一点点绕,可以想象一下,前面的组合排列方式不变,头尾两块变成了一块,我们需要把这种情况的组合都排除出去。

则, A(n)+A(n1)=m(m1)n1 A ( n ) + A ( n − 1 ) = m ∗ ( m − 1 ) n − 1

同时, A(2)=m(m1) A ( 2 ) = m ∗ ( m − 1 ) 只有两块的情况下

再由, A(n)=m(m1)n1A(n1) A ( n ) = m ∗ ( m − 1 ) n − 1 − A ( n − 1 ) ,两边同时减去 (m1)n ( m − 1 ) n ,

得, A(n)(m1)n=[A(n1)(m1)n1] A ( n ) − ( m − 1 ) n = − [ A ( n − 1 ) − ( m − 1 ) n − 1 ]

来,我,我们看一下等号两边的式子,会发现是一个有规律的式子,同时是一个可以产生递推的式子,递推法在后面会讲到。

继而得, A(n)(m1)n=[A(n1)(m1)n1] A ( n ) − ( m − 1 ) n = − [ A ( n − 1 ) − ( m − 1 ) n − 1 ]
           =(1)2[A(n2)(m1)n2] = ( − 1 ) 2 [ A ( n − 2 ) − ( m − 1 ) n − 2 ]
           =(1)3[A(n3)(m1)n3] = ( − 1 ) 3 [ A ( n − 3 ) − ( m − 1 ) n − 3 ]
           =........ = . . . . . . . .
           =(1)n3[A(3)(m1)3] = ( − 1 ) n − 3 [ A ( 3 ) − ( m − 1 ) 3 ]
           =(1)n2[A(2)(m1)2] = ( − 1 ) n − 2 [ A ( 2 ) − ( m − 1 ) 2 ]
      (只剩两块时,m种颜色,共有 A(2)=m(m1) A ( 2 ) = m ∗ ( m − 1 ) 种)
           =(1)n2[m(m1)(m1)2] = ( − 1 ) n − 2 [ m ∗ ( m − 1 ) − ( m − 1 ) 2 ]
           =(1)n(1)2(m1) = ( − 1 ) n ( − 1 ) 2 ( m − 1 )
           =(1)n(m1) = ( − 1 ) n ( m − 1 )
故,      A(n)=(1)n(m1)+(m1)n A ( n ) = ( − 1 ) n ( m − 1 ) + ( m − 1 ) n

NND,忽然有了一种做了高考推导题的既视感,事实证明数学是真的很重要的东西啊!~

代码实现

#include "stdio.h"
#include "math.h"
    int main()
    {
        int split_num;
        int color_num=4; 
        scanf("%d",&split_num); 

        int kind;  
        /*若是split_num和color_num的数字很大的话,计算结果
        * 可能会超出int型的字符长度,故选择double或long型较好。          
        * 注意,这里的测试用例数字很小,不会超出int型的长度,
        * 同时按照用例,输出为int型,若为double或int还需取整操作。故设置为int型*/
        kind = pow(color_num - 1,split_num) + (color_num - 1)*pow(-1,split_num);
        //公式 A(n) =(-1)^n (m-1)+(m-1)^n
        printf("%d",kind);     
    }

方法二:递归法

  当使用递归法的时候,我们首先要考虑他的递归边界,既什么时候停止跳出,设递归方法是recurve(int n,int m),参数分别是分块数和颜色数split_num 和 color_num。
  这里又有了两种思考。先依照上面这一种方法来继续思考他的递归方法。
  

法2-1

  方法一是直接推导出了他的终极公式,直接代入进行计算就可以。但是我们其实在只是一开始找到规律的时候就可以使用递归的思想来进行拆解,一步步的化简来解决子问题。
  
  在方法一中,我们得到这样一个结论,
   A(n)=m(m1)n1A(n1) A ( n ) = m ∗ ( m − 1 ) n − 1 − A ( n − 1 )
  则求解 A(n) A ( n ) 的问题就演变成了求解 A(n1) A ( n − 1 ) 的问题,这样就可以用递归的思想,循环的调用自己进行计算,碰到边界后再返回。

法2-2

   在新涂第N块扇形时,由于新加的第N块扇形的颜色跟旁边两块相关,我们需要考虑两种情况。

  1. 如果旁边两块颜色不同,那么第N块有M-2种颜色,由于两块颜色不同,可以把中间的摘出来,其他N-1块再拼成一个圆,不影响讨论新的圆形求解问题变成了recurve(n-1,m)的问题
    故总共有recurve(n-1,m)*(m-2)种可能;


    染色问题 —— 扇形涂色_第2张图片
    图2. 扇形涂色法2.2图示-1

  2. 如果旁边两块颜色相同,那么第N块有M-1种颜色,由于两块颜色相同,相当于是去掉任意一块,在去掉第N块,重新拼成的一个圆形符合要求,不影响讨论新的圆形的求解问题变成了recurve(n-2,m)的问题,则总共有recurve(n-2,m)*(m-1)种可能;


    染色问题 —— 扇形涂色_第3张图片
    图3. 扇形涂色法2.2图示-2

      然后再继续解决这个两个子问题,函数自己调用自己,即为递归,整个过程我们可以将它理解成是在从第N块到第1块的拆解,推导。
      两个方法的区别一个是加合格的,一个是减不合格的。

代码实现

C++代码实现

    #include "stdio.h"
    #include "math.h"

    int recurve(int n,int m){
        int kinds;
        if(n==1){
            kinds = m;
        }
        if (n==2){
            kinds = m*(m-1);
        }
        if (n==3){
            kinds = m*(m-1)*(m-2);
        }
        if (n>3){
            /*kinds = m*pow(m-1,n-1) - recurve(n-1,m);
                      .............这是方法2.1的递归公式*/
            kinds = recurve(n-1,m)*(m-2) + recurve(n-2,m)*(m-1);
            //这是方法2.2的递归公式
            /*两个递归公式的区别在于,2.1是将在总的数量中减去不合格的数量
             *2.2是分情况讨论,找到符合的情况下的数量进行相加*/
        }
        return kinds;
    }

    int main()
    {
        int split_num;
        int color_num=4; 
        int kind;  
        scanf("%d",&split_num); 
        kind = recurve(split_num,color_num);    
        printf("%d",kind);        
    }

Java代码实现

import java.util.Scanner;
import static java.lang.StrictMath.pow;

public class tryA {
    public static  void main(String[] args){
        Scanner scan = new  Scanner(System.in);
        int  split_num = scan.nextInt();
        int  color_num = 4;

        if(split_num >= 1 && color_num>=1){
            double kind = recurve(split_num,color_num);
            System.out.printf ("%.0f",kind);
        }
    }
    public static double recurve (int N,int M){
        double kind_new = 0;
        if(N == 1 && M >=1)
            return M;
        if(N == 2 && M >=2)
            return M*(M-1);
        if(N == 3 && M>=3)
            return M*(M-1)*(M-2);
        if(N >3 && M >=2){

            /*kind_new= M*pow(M-1,N-1) - recurve(N-1,M);
             *法2.1,该方法在数字较大是优势尤其明显*/
            kind_new=recurve(N-1,M)*(M-2) + recurve(N-2,M)*(M-1);
        }
        return kind_new;
    }
}

算法复杂度分析及改进

  方法2.1的算法,每个recurve方法只被计算了一次,而方法2.2则会被重复计算,以100为例,画图说明一下两种方法的复杂度。


染色问题 —— 扇形涂色_第4张图片
图4. 法2.1递归复杂度分析


染色问题 —— 扇形涂色_第5张图片
图5. 法2.2递归复杂度分析

  由图一图二可以看出,法2.1只需要计算一边所有的数,从100到1,而法2.2则需要越往下计算所需要重复计算的次数也越多,数字越大需要的时间会呈几何上升。
  很明显,法2.2是有很大的优化空间的。有一个思路是我们可以从下往上进行计算,通过93和94计算出95,通过94和95就计算出96,依次计算,直到100,这样子优化每个分块数只需要计算一次就可以,不过我们需要存储一下中间变量,比较好的方式是做一个累加。
  因为这个计算的公式有点麻烦,我没有编程实现,但是方法可以 参考我的斐波那契数的文章,里面有提高运行效率的方法及编程,与之非常类似。后续有时间我会把和这个补上的,如果有大神有兴趣试一下给小弟补上也是不胜感激~

总结

  法一推导过程最复杂,但是编程实现最简单,复杂度也最低,只是一次计算,适合数值比较大的时候,适合数学好的人。
  法2.1推导简单,实现也不复杂,优。
  法2.2为优化前复杂度偏高,思考过程也有点复杂,未改良前,良 - 。
  总的数值减去不符合的 比 符合的情况依次相加 要简单不少。 这个思维可以多多使用。 , 个 人 意 见 , 未 经 验 证 , 慎 采 纳 !

你可能感兴趣的:(经典面试题目解析,算法)