算法 - 练习(1)

  • 填写运算符(穷举)

#include<stdio.h>
//填写运算符(+、-、*、/),使“5?5?5?5?5 = 5”成立。 

void main() 
{
    int num[6]; //保存运算数(5个;num[0]不使用) 
    int result; //保存结果数(1个) 
    char oper[5] = {' ', '+', '-', '*', '/'}; //保存运算符 
    int j; //用作下面所有for循环的循环变量 
    printf("请输入操作数(5个):");
    for(j = 1; j <= 5; j++)
        scanf("%d", &num[j]);
    printf("请输入结果数(1个):");
    scanf("%d", &result); 
	
    float left, right; //保存将要进行计算的左操作数与右操作数 	
    int count = 0;	//计数器,用来统计符合条件的方案的个数 
    int flag; 	//flag为1时,能够保证遇到“+”时做加运算;flag=-1时, 够保证遇到“-”时做减运算
	
    //数组i用来保存4个位置上可以填入的可使等式成立的运算符的代码(1表示+,2表示-,3表示*,4表示/) 
    //这需要下面的for循环来得到一组结果
    //举例来说,i[1]=2则表示第二个位置是-,i[4]=4则表示第四个位置为/,i[0]是不使用的  
    int i[5];
	
    for(i[1] = 1; i[1] <= 4; i[1]++)  //第一个位置上的运算符. i[1]的值从1 ~ 4,即,将所有运算符都放在这里尝试一遍. 下同 
    {
        if (i[1] < 4 || num[2] != 0)  //i[1]<4表示+-*运算符;填入/运算符的前提是除数num[2]不为0. 下同
            for(i[2] = 1; i[2] <= 4; i[2]++) 
            {
                if(i[2] < 4 || num[3] != 0) 
                    for(i[3] = 1; i[3] <= 4; i[3]++)
                    {
                        if(i[3] < 4 || num[4] != 0)
                            for(i[4] = 1; i[4] <= 4; i[4]++)
                            {
                                if(i[4] < 4 || num[5] != 0)
                                //上面的for、if穷举出了一组运算符,下面验证这组运算符是否可使等式成立 
                                {
                                    /* 1、假设先是0与第一个操作数进行加(flag=1)运算。 
                                        * 这里要注意的是:此时还并未运算。
                                        * 只有遇到下一个运算符时才能知道要不要运算。
                                        * 因为如果下一个运算符是乘除,则要先计算乘除,而先不必改变left与flag的值。
                                        * 这也就是下面的for循环做的工作。 
                                    */
                                    left = 0;
                                    right = num[1];
                                    flag = 1;
                                    /* 2、下面按照“先算乘除,后算加减”的规则进行计算,体现在: 
                                        * 加减运算时,左右操作数都改变;
                                            * 新的左操作数通过当前left、right计算得到
                                            * 新的右操作数为num[j+1] 
                                        * 乘除运算时,只需改变右操作数:
                                            * 通过与num[j+1]做乘除运算来改变
                                    */ 
                                    for(j = 1; j<= 4; j++)
                                    {
                                        switch(oper[i[j]])
                                        {
                                            /* 这里需要知道/强调的是: 
                                                * 判断出是加减符号时,并没有立刻做这个加减符号两边的操作数的加减运算,
                                                * 而是做上一次的加减运算改变left。
                                                * 之所以这样做,是因为可能再下一次就会遇到乘除运算。
                                                * (如果遇到了乘除运算符,那就是立刻运算这个符号两边的操作数了,即:
                                                * right = right * num[j + 1] 或 right = right / num[j + 1];) 
                                                * 如果不是上面说的那样,那就成了不管加减与乘除的运算顺序,
                                                * 直接从左到右一次进行运算了,这显然是不对的。
                                                * 请自己举例进行验证。 你讲更明白此过程。 
                                            */
                                            case '+':
                                                left = left + flag * right;
                                                flag = 1;
                                                right = num[j + 1];
                                                break; 
                                            case '-':
                                                left = left + flag * right;
                                                flag = -1;
                                                right = num[j + 1];
                                                break;
                                            case '*':
                                                right = right * num[j + 1];
                                                break;
                                            case '/':
                                                right = right / num[j + 1];
                                                break;	
                                            } //switch 
                                        } //for(int j = 1; j<= 4; j++)
										
                                        // 3、做最后一次左右操作数的加或减(由flag决定)运算,判断最终结果 
                                        if (left + flag * right == result)
                                        {
                                            count++;
                                            printf("%3d:", count);
                                            for(j = 1; j <= 4; j++)
                                                printf("%d %c ", num[j], oper[i[j]]);
                                            printf("%d = %d\n", num[5], result);											
                                        }
                                    }
                            } //for(i[4] = 1; i[4] <= 4; i[4]++)
                    } //for(i[3] = 1; i[3] <= 4; i[3]++)
            } //for(i[2] = 1; i[2] <= 4; i[2]++)
    } //for(i[1] = 1; i[1] <= 4; i[1]++)
	
    if (count == 0)
        printf("没有符合要求的运算符组合!\n"); 
} 
/* 下面举两个实例来看一下穷举过程:
1、 5 + 5 * 5 / 5 - 5 = 5 			|	2、 5 * 5 * 5 / 5 / 5 = 5 
	    left    right			|    		left     right
	    0	    num[1]			|		0	    num[1]*num[2]=25 
	    5	    num[2]			|		0	    25*num[3]=125
	    5	    num[2]*num[3]=25	        |		0	    125/num[4]=25
	    5	    25/num[4]=5		        |		0	    25/num[5]=5
	    10	    num[5]			|		0+5=5符合 
	    10-5=5符合				|	
*/ 

  • 猴子吃桃问题(逆推)

1)弄清如果由人来做,应该采取那些步骤;
2)对这些步骤进行归纳整理,抽象出数学模型;
3)对其中的重复步骤,通过使用相同变量等方式,求得形式的统一,然后简练的用循环解决。

#include<stdio.h>

void main()
{
    int day, a, b;
    day = 9;
    b = 1;
    while(day > 0)
    {
        a = ( b + 1 ) * 2;
        b = a;
        day--;
    }
    printf("%d", a);
} 
  • 汉诺塔问题(递归)

       参考书上的例子是能够让人理解的:假设有n个盘子需要从第一个柱子移动到第三个柱子,那么“我本人”只负责将第一个柱子上的最后一个柱子移动到第三个柱子上,“我”可以先命令另一个人将那n-1个柱子借助第三个柱子移动到第二个柱子上,然后我就可以挪最后一个柱子到第三个柱子上了,然后我再命令此人将这n-1个盘子借助第一个柱子移动到第三个柱子上;此人也无法完成自己的任务,于是将任务继续下发给另一人......直至任务不需再细分。

       如果有两个盘子,则需要3步;如果有3个盘子,则需要3+1+3=7步;如果有四个盘子,则需要7+1+7=15步...如果有n个盘子,则需要2^n-1步,步骤如下:

  1. 把1座上的n-1个盘子借助3座移到2座;
  2. 把1座上第n个盘子移到3座;
  3. 把2座上n-1个盘子借助1座移到3座。

程序如下:

#include<stdio.h>
void move(int n, int x, int y, int z);

void main()
{
    int h;
    printf("请输入盘子个数:");
    scanf("%d", &h);
    printf("盘子移动步骤如下:\n");
    move(h, 'a', 'b', 'c');
}
void move(int n, int x, int y, int z)
{
    if (n == 1)
        printf("%c ---> %c\n", x, z);
    else
    {
        move(n-1, x, z, y);         //把x座上的n-1个盘子借助z座移到y座;
        printf("%c ---> %c\n", x, z);//把x座上第n个盘子移到z座;
        move(n-1, y, x, z);	    //把y座上n-1个盘子借助x座移到z座。
    }
}

  • 找出假币(分治)

判断假币的存在(假币比真币轻):

  • 非分治法:一共8个硬币,分成四组,每一组有两个硬币。分别比较每个组里的两个硬币的轻重,最少比较一次即可得到答案,最多比较4次才可得到答案。
  • 分治法:一共8个硬币,分成两组,每一组有四个硬币。比较两组的轻重,即可一次性得到答案。

找出假币:

  • 非分治法:与上面判断假币的存在一样。因为上面不仅判断了出来,还能直接确定哪个是假币了。需比较1~4次。
  • 分治法:8个硬币,分成两组,每一组有四个硬币。轻的那一组,继续分成两组...直至两组中每组只有1个硬币,即可找出假币。需比较3次。

下面的程序考虑的是很全面的(看网上的例子完善了自己初步写的,然后又进一步完善了网上的程序,使得包括没有假币的情况):

#include<stdio.h>
#include<time.h>
#include<stdlib.h> 
#define N 7   //硬币个数 

int findFake(int coins[], int low, int high);

int main()
{
    int coins[N] = {1, 1, 1, 1, 1, 1, 1};
    int num; //num是假币的索引值 
    if(N == 1)
    {
        printf("只有一个硬币,无法判断其真假!");
        return 0;
    }
    num = findFake(coins, 0, N-1);
    if (num == -1)
        printf("没有假币!");
    else 
        printf("第%d个硬币是假的!", num+1);
    return 1;
}
/*
    数组中总硬币的个数可以用low和high进行计算得到!这样更容易标识某组硬币!
    否则每次都要重新保存出一个要进行判断的硬币数组! 
    @ coins[]:要进行判断的一组硬币
    @ low:索引的起始位置 
    @ high: 索引的结束位置
    @ return:返回假币所在的索引值 
*/ 
int findFake(int coins[], int low, int high) 
{
    int i, sum1 = 0, sum2 = 0;  //sum1是第一组硬币的重量之和;sum2是第二组硬币的重量之和 
    int mid = ((high + low) + 1) / 2;

    if (high - low == 1 && coins[low] != coins[high])	//硬币总数只有两个 
        return coins[low] > coins[high] ? high : low;
    if (high - low == 1 && coins[low] == coins[high])
        return -1;
		
    if (((high - low) + 1) % 2 == 0)//硬币总数为偶数
	{
//      printf("偶数...low: %d   high: %d\n", low, high); //用来检查程序哪里出错了 
        for(i = low; i < mid; i++)		
            sum1 += coins[i];
        for(i = mid; i <= high; i++)
            sum2 += coins[i];
        if (sum1 != sum2)
            return sum1 < sum2 ? findFake(coins, low, mid-1) : findFake(coins, mid, high);	
        else
            return -1;				
    }
    else 							//硬币总数为奇数
    {
//      printf("奇数...low: %d   high: %d\n", low, high); //用来检查程序哪里出错了 
        for(i = low; i < mid; i++)		
            sum1 += coins[i];
        for(i = mid; i < high; i++) //i <= (high - 1)是因为最后一个硬币先不参与判断 
            sum2 += coins[i];	
        if (sum1 != sum2)  //如果sum1 != sum2,则假币一定在其中一组内 
            return sum1 < sum2 ? findFake(coins, low, mid-1) : findFake(coins, mid, high-1);
        else //如果sum1 == sum2,则可能不存在假币,也可能最后一个是假币。下面进行判断 
        {
            srand(time(0));
            int aRealCoin = rand() % (mid - low) + low; //随机取一个真币与最后一个假币进行比较 
            if (coins[aRealCoin] != coins[high]) 
                return high;
            else
                return -1;
        }
    } 
}
  • 大数相乘(分治)

在搜索资料时,发现大数有四则运算——加减乘除,所以决定都看一下吧。

所谓大数,就是指数字比较大,运算的结果超出了基本类型的表示范围,所以这样的数不能够直接做运算,而是应该将这些大数使用字符串数组保存起来。这就要求我们将大数的整体运算转换为每一个数组元素的运算,难点就在这一转换上 。


        1. 首先看最为简单的大数加法

将用户输入的两个运算数保存在数组add1与数组add2中(假设add1 = {1, 2, 3, 4, 5, 6, 7, 8, 9}; add2 = {1, 2, 3, 4})。如果模仿手工计算,从低位到高位依次相加,满十则进一,那么有两个问题需要解决:

  • 保存结果的数组的长度是多少位? 
  • 如何写一个满十进一的算法? 

答案其实很简单:

  • 两个数相加的结果的位数最大只会比较大的那个运算数的位数多一位,所以:用lenSum代表结果的长度,len1代表add1的长度,len2代表add2的长度,则 lenSum = len1 > len2 ? len1 : len2 ; ++lenSum即为结果数组的长度。 
  • 如果每加一位就判断是否有进位的话问题就会复杂一点,所以我们可以先保存每一位相加的结果,最后再对结果进行处理。如下所示。
//保存每一位相加的结果:
      1  9  8
+     3  2  1
-------------
     (4)(11)(9)  <----相对应位的和,还没有累加进位

//依次累加进位:
for(i = 0; i < lenSum; i++)
{    
    if(result[i] > 9)
    {
        result[i] = result[i] % 10;
        result[i+1] += 1;
    }
}

总程序的实现如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define MAXSIZE 10

void bigNumAdd(char add1[], char add2[], int len1, int len2, char result[], int lenSum);

void main()
{
    char add1[MAXSIZE], add2[MAXSIZE], *result;
    int len1, len2, lenSum;
	
    printf("请输入第一个运算数:");
    scanf("%s", add1);
    getchar();
    printf("请输入第二个运算数:");
    scanf("%s", add2);
    getchar();	

    len1 = strlen(add1);
    len2 = strlen(add2);
    lenSum = len1 >= len2 ? len1+1 : len2+1;          //加1是因为可能有个最后的进位
    result = (char *)malloc(sizeof(char) * lenSum+1); //加1是因为最后需有结尾符'\0' 
	
    bigNumAdd(add1, add2, len1, len2, result, lenSum);
    printf("%s + %s = %s", add1, add2, result);
	
    free(result);
}
void bigNumAdd(char add1[], char add2[], int len1, int len2, char result[], int lenSum)
{
    //三个循环变量
    int i, j, k = 0; 
    /*
    @ finalResult[MAXSIZE] 将保存最终正确的运算结果,但还是需要将这个值复制给result,供主函数得到    
    @ add11[MAXSIZE]/add22[MAXSIZE] 是add1/add2的副本,以防下面的程序会改动原始数据add1、add2(是的,如果不设置副本,会改动原始数据,使得程序出现错误!这也是自己在最终测试时发现的。至于是何种错误,下面的注释里会说到~)
    */
    char finalResult[MAXSIZE], add11[MAXSIZE], add22[MAXSIZE]; 
    strcpy(add11, add1); 
    strcpy(add22, add2);

    /* 1、【非常关键的一步】
        * 因为以char类型存储的数字以%d输出时,输出的是该数字对应的ASCII码;
        * 所以这里通过与字符0做差运算,就可以使在以%d输出时得到真正的做运算的数字!
        * 另外,也就是在这一步,对add1、add2做了改动(但已经替换成了副本了,所以程序是正确的):
            * 做了“ -'0' ”运算后,再以%c或%s输出此字符时就会出现乱码!
            * 如果想要得到正确的,那么就要做“ +'0' ”运算...
    */
    for (i = 0; i < len1; i++)
    {		
        add11[i] = (add11[i] - '0'); 
//      printf("%s —— %d\n", add11, add11[i]);  // 出现乱码!
    } 
    for (i = 0; i < len2; i++)
    {
        add22[i] = add22[i] - '0';
    }

    /* 2、【对应位求和】
        * 注意:最后一位的求和结果放在了result[0],而不是放在了result的最后一个位置。
    */
    for(i = len1-1, j = len2-1; i >= 0 && j >= 0; i--, j--)
    {		
        result[k++] = add11[i] + add22[j];	
        printf("result[%d]: %d\n", k-1, result[k-1]); //测试
    }

    // 3、【将较大数的其他未参与运算的位写入result[]】
    if (len1 > len2)
    {
        for(i = len1-len2-1; i >= 0; i--)
        {
            result[k++] = add11[i];
            printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
        }
    } 
    if (len1 < len2)
    {
        for(i = len2-len1-1; i >= 0; i--)
        {
            result[k++] = add22[i];	
            printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
        }
    }
    result[k] = '0' - '0';
    printf("result[%d]: %d\n", k, result[k]); //测试
    result[lenSum] = '\0';

    //4、【实现进位】
    for(i = 0; i < lenSum; i++)
    {    
        if(result[i] > 9)
        {
            result[i] = result[i] % 10;
            result[i+1] += 1;
        }
    }

    //5、【将最终结果保存到finalResult中(注意循环变量i的设定)】
	i = result[lenSum-1] != 0 ? lenSum-1 : lenSum-2; //为了删除前导0
	for(i, j = 0; i >= 0; i--, j++)
	{		
        //你也可以以这样的形式预览一下最终结果~
        //printf("result[%d]: %d\n", i, result[i]);
 
		finalResult[j] =  result[i] + '0';
	}
    //预览finalResult,看是否正确:
	//printf("%s", finalResult);

    //6、【因为最终结果是主函数中的result携带的,所以进行下面的操作~】
	strcpy(result, finalResult);
}

 

程序中几个需要注意的地方:

  • 因为存储在char数组中的数字以%d输出时,输出的是该数字对应的ASCII码;而我们希望在以%d输出add1[i]时就可以得到真正的做运算的数,那么如何实现这一效果呢?那就是令数字字符与0字符'0'做差运算!相反的,如果想将该数字能以%c输出来,则需要与'0'做和运算,也就是算法中最后需要做的。
  • 最终结果的前导0我们如何去掉(而不会使出现在中间位置的0消失)?答案就是:只需判断最高位上是否为0,如果为0(即说明最高位相加后没有进位),我们就从下一位开始输出。这体现在程序中输出最终结果时循环变量i的设定。

       2.  大数减法

需要解决的问题: 

  1. 结果最多有多少位? 
  2. 借位的算法如何实现? 
  3. 如果一个8位数减去4位数,那么8位数较高的4位如何处理? 

问题的解决:

  1. 结果的最大位数和较大的一个运算数的位数相同。
  2. 同加法类似,先存储每位相减的结果然后再用一个循环作整理:
  3. 可以把被减数缺少的位数用零补全,然后相减;也可以只减到被减数的位数,然后将减数的高位直接写道结果的数组中。
//保存每一位相减的结果:
   1   1   9   8
+      3   2   1
-----------------
  (0) (-2)(7) (7)  <----相对应位的差,还没有借位

//借位:
for(i = 0; i < lenSub; i++)
{    
    if(result[i] < 0)
    {
        result[i] = result[i] + 10;
        result[i+1] -= 1;
    }
}

注意:做减法运算时,如果遇到了小数减去一个大数,那么在计算时还是要大的减小的,然后添加一个符号。所以在运算时,必须要先判断两个数的大小!所以必须分别讨论各种情况。代码实现如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define MAXSIZE 10

void bigNumSub(char sub1[], char sub2[], int len1, int len2, char result[], int lenSub);

int main()
{
    char sub1[MAXSIZE], sub2[MAXSIZE], *result;
    int len1, len2, lenSub;
	
    printf("请输入第一个运算数:");
    scanf("%s", sub1);
    getchar();
    printf("请输入第二个运算数:");
    scanf("%s", sub2);
    getchar();	

    if(strcmp(sub1, sub2) == 0)
    {
        printf("%s - %s = 0", sub1, sub2);
        return 0;
    }

    len1 = strlen(sub1);
    len2 = strlen(sub2);
    lenSub = len1 >= len2 ? len1+1 : len2+1; //加1是因为在最前面添加个正负号 
    result = (char *)malloc(sizeof(char) * (lenSub+1)); //加1是因为最后需有结尾符'\0' 
    int i;
    for(i = 0; i < lenSub; i++) 
    	result[i] = '0' - '0'; 
        //这样能保证以%d输出时为数字0!
        //最好这样初始化,反正最终结果是由下面函数finalResult复制过来的,
        //所以result如何初始化都没关系,但这样是为了预防需要以%d形式输出。
    result[lenSub] = '\0';
	
    bigNumSub(sub1, sub2, len1, len2, result, lenSub);
    printf("%s - %s = %s", sub1, sub2, result);
	
    free(result);
    return 0;
}
void bigNumSub(char sub1[], char sub2[], int len1, int len2, char result[], int lenSub)
{
    int i, j, k = 0; 
    int flag = 0; // flag=0表示int(sub1) > int(sub2); flag=0表示int(sub1) < int(sub2)。从而知道差运算结果是正还是负 
    char finalResult[MAXSIZE], sub11[MAXSIZE], sub22[MAXSIZE];
    strcpy(sub11, sub1);
    strcpy(sub22, sub2);
    
    for (i = 0; i < len1; i++)
    {		
        sub11[i] = sub11[i] - '0'; 
    } 
    for (i = 0; i < len2; i++)
    {
        sub22[i] = sub22[i] - '0';
    }
    	
    	
    //注意注意:虽然最终返回的result带有正负号,但是下面在计算result的过程中,并没有管正负号;
    //因为最后的正负号只需放在finalResult[0]出,然后得出最终的结果的字符串,再复制到result即可。
    //所以对于主函数的result,还是要算上正负的位置的。 
    if(len1 > len2)
    {
        //对应位求差
        for(i = len1-1, j = len2-1; i >= 0 && j >= 0; i--, j--)
        {
            result[k++] = sub11[i] - sub22[j];
            printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
        }
        //将较长位数的运算数的未参加运算的几项添加到result中
        for(i; i >= 0; i--)
        {
            result[k++] = sub11[i];
            printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
        }		
    }
	
    if(len1 < len2)
    {
        flag = 1;
        //对应位求差
        for(i = len1-1, j = len2-1; i >= 0 && j >= 0; i--, j--)
        {
            result[k++] = sub22[j] - sub11[i];
            printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
        }
        //将较长位数的运算数的未参加运算的几项添加到result中
        for(j; j >= 0; j--)
        {
            result[k++] = sub22[j];
            printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
        }
    }
	
    if(len1 == len2)
    {
        for(i = 0, j = 0; i <= len1; i++, j++)
        {
            if (sub11[i] == sub22[j])
                continue;
            if (sub11[i] > sub22[j])
            {
                //对应位求差
                for(i = len1-1, j = len2-1; i >= 0 && j >= 0; i--, j--)
                {
                    result[k++] = sub11[i] - sub22[j];
                    printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
                }
                break;
            }
            if (sub11[i] < sub22[j])
            {
                flag = 1;
                //对应位求差
                for(i = len1-1, j = len2-1; i >= 0 && j >= 0; i--, j--)
                {
                    result[k++] = sub22[j] - sub11[i];
                    printf("result[%d]: %d\n", k-1, result[k-1]); //测试	
                }		
                break;		
            }
        }
    } 
	
	
    //借位
    for(i = 0; i < lenSub; i++)
    {
        if(result[i] < 0)
        {
            result[i] = result[i] + 10;
            result[i + 1] -= 1;
            //printf("%d", result[i]); //测试 
        } 
    }
    printf("\n");
	
	
    //保存正确结果的字符串到finalResult中
    if(flag == 0 )
        finalResult[0] = '+';
    else
        finalResult[0] = '-';
    j = result[lenSub - 1] == 0 ? lenSub - 2 : lenSub - 1;
    for(i = 1, j; j >= 0; i++, j--)
    {
	    //printf("%d", result[j]);   //测试 
	    finalResult[i] = result[j] + '0';
    }
    //printf("%s\n", finalResult);   //测试 
    strcpy(result, finalResult);
} 


      3. 大数乘法 

需要解决的问题: 

  1. 我们要用多大的数组存储结果? 
  2. 要使用嵌套循环吗? 
  3. 如何在计算的过程中保证进位?

问题的解决:

  1. 二个数相乘最大的位数是两个乘数的位数之和。 
  2. 很明显由于乘法的特性使用嵌套循环很合适。 
  3. 可以每做一次乘法运算就解决此次的进位,也可以全部运算完毕后最后在同一解决进位。

事实上,上面的代码实现的原理与加减类似(此时还未使用分治法解决大数乘法)。这里不再实现(可以看这里)...

下面我们主要看利用分治法如何解决?那么你知道为何推崇分治法吗?无非就在于时间复杂度或空间复杂度了。普通的乘法计算方式,时间复杂度都是O(n^2),而使用分治法,时间复杂度仅有 O(n^1.59)。 

然而,想要使用分治法写出大数乘法的算法并不容易,因为首先你需要知道一些数学上的推论,这是是实现此算法的理论依据。下面通过实例来进行原理的介绍:

假设我们计算x * y,其中x = 5678, y = 1234;那么按照我们正在讲述的原理,应该这样做:

  • 首先将5678、1234拆成a、b、c、d部分(一定是部分,这是我们正在讲述的定理所要求的,即每个数都被拆成前后两部分)(如下图):

  • 然后, 分别计算a*c,  b*d,  (a+b)*(c+d);
  • 然后,计算③-②-①(其实得到的就是b*c+a*d,只是这样做减少了乘法运算,要知道,做加减法可比做乘除法容易的多,对于计算机也是这样的,时间复杂度也就减少了);
  • 最后,第一个算式的结果后面加4个0(因为实际上是5600 * 1200),第二个算式的结果不变,第四个算式后面加2个0,再把这三者相加,就是正确结果。

下面,我们给出定理(或者直接说是算法的步骤):

对图的解释如下:

我们假设要相乘的两个数是x、y。那么我们可以把x、y写成(n是x、y的位数,这里要保证x、y的位数都为n;至于位数不相同时的算法的分析,请看这里):

 即,把x、y分成两部分——如果n是偶数,则a和b都是n/2位的。如果n是奇数,则你可以让a是n/2+1位,b是n/2位。这样,x * y就变成了:

进一步计算,得:

 再结合实例,大数相乘的分治法算法已经呼之欲出了。不过,这里需要强调一点:这一运算过程在算法里是递归实现的,数字很大时,拆分后可能继续需要拆分,直至a * b已经是一个非常简单的小问题(比如,是一个一位数乘一位数的运算)为止,这也就是分治的思想。

但是我们再来看看,我们是否可以用加法来换取乘法?因为多一个加法操作,也是常数项,对时间复杂度没有影响,如果减少一个乘法则不同。答案是可以的,如下:

【 (A - B)(D - C) + AC + BD 也可以写成  (A + B)(C + D) - AC - BD)】

至此,你应该已经知道了使用分治的方式计算乘法的原理。另外说一下,上面这个算法,是由 Anatolii Alexeevitch Karatsuba 于1960年提出并于1962年发表,所以也被称为 Karatsuba 乘法。如果想了解时间复杂度的计算,请看这里

下面我们用代码来实现此算法,但是在此之前,我得说一下:

看了网上几个分治法解决大数相乘的算法,发现各不相同。比如有的算法就只写死——只分治一次,这种情况下,可以用char数组保存运算数,也可以用long型保存运算数。如果用char数组保存运算数(Here),那么运算结果可以按照上面实现大数加减的原理来实现乘法;如果使用long型(Here),则直接使用“*”号给“分治”了的数字做乘法运算。还有一种算法则是可以分治多次(Here),但这种情况以我现在的认知来说,我认为必须要把运算数保存为long型,否则无法返回结果呀,且返回的结果也是long型的。

下面我就把这两种算法都放在这里:

  • 只分治一次(用char数组保存运算数)
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define MAXSIZE 10
void bigNumMulti(char *x, char *y);

int main()
{
    char x[MAXSIZE], y[MAXSIZE];
	
    //此程序只计算两个正数相乘的结果(两个数相乘的符号自己很容易判断出来哦) 
    //也不考虑输入的运算数是0或1的情况 
	
    printf("请输入第一个运算数的绝对值:"); 
    scanf("%s", x);
    getchar();
    printf("请输入第二个运算数的绝对值:");
    scanf("%s", y);
    getchar();
    
    bigNumMulti(x, y);
}
char * bigNumMulti(char *x, char *y)
{
    char *result;
    int len1, len2, len, len1l, len1r, len2l, len2r;
    int i;
	
    len1 = strlen(x);
    len2 = strlen(y);
    len = len1 + len2; 
    result = (char *)malloc(sizeof(char) * (len+1)); //加1是因为最后需有结尾符'\0'     
    for(i = 0; i < len; i++)
    	result[i] = '0' - '0';  
    result[len] = '\0';
    
	
    len1l = len1 / 2;
    len1r = len1 - len1l;
    len2l = len2 / 2;
    len2r = len2 - len2l;
    
    char A[len1l+1], B[len1r+1], C[len2l+1], D[len1r+1];
    for (i = 0; i < len1l; i++)
    {
        //保存分治出的数。。。	
    } 
    A[len1l] = '\0';
	
    //做乘法计算。因为保存入了char数组里,所以计算原理同大数加减。
	
    //结果整合 
} 
  • 分治多次
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
 
void SameNumber(); //两个运算数的长度相同时的输入算法
long CalculateSame(long X, long Y, int n);//两个运算数的长度相同时的计算算法 
void UnSameNumber();//两个运算数的长度不相同时的输入算法
long CalculateUnSame(long X, long Y, int xn, int yn);//两个运算数的长度相同时的计算算法  
int SIGN(long A);//用来判断最终结果是正还是负 
 
int main()
{
    SameNumber();
    UnSameNumber();
    return (0);
}
 
int SIGN(long A)
{
    return A > 0 ? 1 : -1;
}
 
void SameNumber()
{
    long X = 0,Y = 0;
    int n = 0;
    
    printf("------------理想状态下用法---------\n");
    printf("两个运算数的长度均为:");
	scanf("%d",&n); 
    printf("请输入第一个运算数:");
    scanf("%d", &X);
    printf("请输入第二个运算数:");
    scanf("%d", &Y);
    
    long sum = CalculateSame(X, Y, n);
 
    printf("普通乘法 X * Y = %d * %d = %d\n", X, Y, X*Y);
    printf("分治乘法 X * Y = %d * %d = %d\n", X, Y, sum);
}
 
long CalculateSame(long X, long Y, int n)
{
    int sign = SIGN(X) * SIGN(Y);
 
    X = labs(X); //求绝对值 
    Y = labs(Y);
    
    if (X == 0 || Y == 0)
        return 0;
    else if (n == 1)        
        return sign * X * Y;
    else
    {
        long A = (long)(X / pow(10, n / 2)); //pow(x, y)表示求x的y次方 
        long B = (X % (long)pow(10, n / 2));
        long C = (long)(Y / pow(10, n / 2));
        long D = (Y % (long)pow(10, n / 2));
        
        printf("A=%d, B=%d, C=%d, D=%d\n", A, B, C, D);
 		
        long AC = CalculateSame(A, C, n / 2);
        long BD = CalculateSame(B, D, n / 2);
        long ABCD = CalculateSame((A - B), (D - C), n / 2) + AC + BD;       
 
        return (long)(sign * (AC * pow(10, n) + ABCD * pow(10, n / 2) + BD));
    }
}
 
 
void UnSameNumber()
{
    long X=0,Y = 0;
    int xn=0,yn=0;
    
    printf("------------非理想状态下用法---------\n");
    printf("请输入第一个运算数:");
    scanf("%d", &X);
    printf("请输入第二个运算数:");
    scanf("%d", &Y);
    printf("请输入第一个运算数的长度:");
    scanf("%d", &xn);
    printf("请输入第二个运算数的长度:");
    scanf("%d", &yn);
 
    long sum = CalculateUnSame(X, Y, xn, yn);
 
    printf("普通乘法 X * Y = %d * %d = %d\n", X, Y, X*Y);
    printf("分治乘法 X * Y = %d * %d = %d\n", X, Y, sum);
}
 
long CalculateUnSame(long X, long Y, int xn, int yn)
{
    if (X == 0 || Y == 0)
        return 0;
    else if ((xn == 1 && yn == 1) || xn == 1 || yn == 1)
        return X * Y;
    else
    {
        int xn0 = xn / 2, yn0 = yn / 2; //如果是奇数的话,除以2得出来的结果是少的那一块 
        int xn1 = xn - xn0, yn1 = yn - yn0; 
 
        long A = (long)(X / pow(10, xn0)); //除以的少的那一块,得出来的结果的长度是xn1,是较长的那一半数 
        long B = (long)(X % (long)pow(10, xn0));
        long C = (long)(Y / pow(10, yn0));
        long D = (long)(Y % (long)pow(10, yn0));
 
        printf("A=%d, B=%d, C=%d, D=%d\n", A, B, C, D);
 
        long AC = CalculateUnSame(A, C, xn1, yn1);
        long BD = CalculateUnSame(B, D, xn0, yn0);
        long ABCD = CalculateUnSame((long)(A * pow(10, xn0) - B), (long)(D - C * pow(10, yn0)), xn1, yn1);
 
        return (long)(2 * AC * pow(10, (xn0 + yn0)) + ABCD + 2 * BD);
    }
}

参考“文献”:

C语言大数运算

大数乘法问题及其高效算法 

分治法的经典问题——大数相乘

  • 世界杯比赛日程安排(分治)

下面直接看程序(需要注意的问题已写成注释;但需要先明确一点:写出此算法关键部分的前提是你要知道对于一个4*4的日程表来说有对称关系!且任意一个更大的日程表都可以分解成4*4的日程表,即按照4*4日程表的规律建立出来。这告诉我们,解决问题之前应该先将问题简化并发现其中的规律): 

# include<stdio.h>
#define MAX_NUM 64

extern int result[MAX_NUM+1][MAX_NUM+1] = {0};
void scheduleArrange(int k, int num); //处理编号k开始的num个球队的日程 

int main(){
    int num, i, j;
    printf("请输入参赛球队数num(num须为2的整数次幂,且不超过64):");//因为后面总是对半分治,所以要求总是为2的倍数 
	
	
    while(1)
    {
        scanf("%d", &num);
        //下面判断输入的数符不符合要求,不符合要求的话,提示重新输入
        i = 2, j = 2;
        for (i; i < 8; i++)//i最大等于7,此时刚好为64
        {
            j = j *2;
            if(j == num) 
                break;	
        } 
        if(j == num) 
            break;
        else
            printf("您输入的数不符合规定,请重新输入:");
    } 
    printf("\n");
	
    //建立result表 
    scheduleArrange(1, num);
 
    //打印表的首行 
    printf("球队编号");
    for(i = 1; i < num; i++)
    {
        printf("\t第%d天", i);
    }
    printf("\n");
	
    //打印result结果表 
    for(i = 1; i <= num; i++)
    {
        for(j = 1; j <= num; j++)
        {
            printf("\t%d", result[i][j]);
        }
        printf("\n");
    }
	
    return 0;
}

void scheduleArrange(int k, int num) //处理编号k开始的num个球队的日程 
{
    int i, j;
	
    if (num == 2) //num为2时不需要再调用scheduleArrange(),即不需要再分治,可直接得出结果 
    {
        //填充对于每个4*4表来说的左半部分 
        result[k][1] = k;		//参赛球队的编号k 
        result[k][2] = k+1;		//k队对阵球队的编号(k+1) 
        result[k+1][1] = k+1;	//参赛球队的编号(k+1)
        result[k+1][2] = k;		//(k+1)队对阵球队的编号k
    }
    else		//只要num不为2,就继续分治 
    {
        scheduleArrange(k, num / 2);
        scheduleArrange(k + num / 2, num / 2);
        //填充(4*4表的)右半部分的上部分 
        for(i = k; i < k + num / 2; i++) //从第k行至中间一行 
        {
            for(j = num / 2 + 1; j <= num; j++) //从中间一列至最右列 
            {
                result[i][j] = result[i + num / 2][j - num / 2];//因为有对称关系,所以下标的关系如此
            }
        }
        //填充(4*4表的)右半部分的下部分 
        for(i = k + num / 2; i < k + num; i++)//从中间一行至最后一行 
        {
            for( j = num / 2 + 1; j <= num; j++)//从中间一列至最右列  
            {
                result[i][j] = result[i - num / 2][j - num / 2];//因为有对称关系,所以下标的关系如此	
            }	
        } 
    } 
} 

程序已看懂,也已举例看过程是怎样的。但如果自己写的话还是有一定难度的。最重要的是发现规律吧,这样才能建立关系,找到关系(如上图,黄色对角线都是对称部分。这也是程序中for循环能那样建立的根据)后,才能正确的建立出日程表。

下面看一个实例过程(需要注意的问题也已写在下图上):

 

发布了53 篇原创文章 · 获赞 33 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/jy_z11121/article/details/96905363