习题解答提要
习题1
1-1 分数分解算法描述
把真分数a/b分解为若干个分母为整数分子为“1”的埃及分数之和:
(1) 寻找并输出小于a/b的最大埃及分数1/c;
(2) 若c>900000000,则退出;
(3) 若c≤900000000,把差a/b-1/c整理为分数a/b,若a/b为埃及分数,则输出后结束。
(4) 若a/b不为埃及分数,则继续(1)、(2)、(3)。
试描述以上算法。
解:设 (这里int(x)表示取正数x的整数),注意到 ,有
算法描述:令c=d+1,则
input (a,b)
while(1)
{c=int(b/a)+1;
if(c>900000000) return;
else
{ print(1/c+);
a=ac-b;
b=bc; // a,b迭代,为选择下一个分母作准备
if(a==1)
{ print(1/b);return;}
}
}
1-2 求出以下程序段所代表算法的时间复杂度
(1)m=0;
for(k=1;k<=n;k++)
for(j=k;j>=1;j--)
m=m+j;
解:因s=1+2+…+n=n(n+1)/2
时间复杂度为O(n2)。
(2)m=0;
for(k=1;k<=n;k++)
for(j=1;j<=k/2;j++)
m=m+j;
解:设n=2u+1,语句m=m+1的执行频数为
s=1+1+2+2+3+3+…+u+u=u(u+1)=(n−1)(n+1)/4
设n=2u,语句m=m+1的执行频数为
s=1+1+2+2+3+3+…+u=u2=n2/4
时间复杂度为O(n2)。
(3)t=1;m=0;
for(k=1;k<=n;k++)
{t=tk;
for(j=1;j<=kt;j++)
m=m+j;
}
解:因s=1+2×2!+ 3×3!+…+ n×n!=(n+1)!−1
时间复杂度为O((n+1)!).
(4)for(a=1;a<=n;a++)
{s=0;
for(b=a100−1;b>=a100−99;b−=2)
{for(x=0,k=1;k<=sqrt(b);k+=2)
if(b%k0)
{x=1;break;}
s=s+x;
}
if(s50)
printf("%ld
",a);break;}
}
解:因a循环n次;对每一个a,b循环50次;对每一个b,k循环 次。因而k循环体的执行次数s满足
时间复杂度为O( )。
1-3 若p(n)是n的多项式,证明:O(log(p(n)))=O(logn)。
证:设m为正整数,p(n)=a1×nm+a2×nm-1+…+am×n,
取常数c>ma1+(m-1)a2+…+am, 则
log(p(n))=ma1×logn+(m-1)a2×logn+…=(ma1+(m-1)a2+…)×logn
<clogn
因而有O(log(p(n)))=O(logn)。
1-4 构建对称方阵
观察图1-5所示的7阶对称方阵:
图1-5 7阶对称方阵
试构造并输出以上n阶对称方阵。
解:这是一道培养与锻炼我们的观察能力与归纳能力的案例,一个一个元素枚举赋值显然行不通,必须全局着眼,分区域归纳其构造特点,分区域枚举赋值。
(1) 设计要点
设方阵中元素的行号为i,列号为j。
可知主对角线:i=j;次对角线:i+j=n+1。两对角线赋值“0”。
按两条对角线把方阵分成上部、左部、右部与下部4个区,如图1-6所示。
图1-6 对角线分成的4个区
上部按行号i赋值;下部按行号函数n+1-i赋值。
左部按列号j赋值;右部按列号函数n+1-j赋值。
(2) 程序实现
#include <stdio.h>
void main()
{int i,j,n,a[30][30];
printf(" 请确定方阵阶数n: ");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{if(i==j || i+j==n+1)
a[i][j]=0; // 方阵对角线元素赋值
if(i+j<n+1 && i<j)
a[i][j]=i; // 方阵上部元素赋值
if(i+j<n+1 && i>j)
a[i][j]=j; // 方阵左部元素赋值
if(i+j>n+1 && i>j)
a[i][j]=n+1-i; // 方阵下部元素赋值
if(i+j>n+1 && i<j)
a[i][j]=n+1-j; // 方阵右部元素赋值
}
printf(" %d阶对称方阵为:
",n);
for(i=1;i<=n;i++)
{ for(j=1;j<=n;j++) // 输出对称方阵
printf("%3d",a[i][j]);
printf("
");
}
}
1-5 据例1-2的算法,写出求解n个“1”组成的整数能被2011整除的程序。
修改程序,求出 n至少为多大时,n个“1”组成的整数能被2013整除?
解:程序为
#include <stdio.h>
void main()
{ int a,c,p,n;
p=2011;
c=1111;n=4; // 变量c与n赋初值
while(c!=0) // 循环模拟整数竖式除法
{ a=c*10+1;
c=a%p;
n=n+1; // 每试商一位n增1
}
printf(" 由 %d 个1组成的整数能被 %d 整除。
",n,p);
}
习题2
2-1 解不等式
设n为正整数,解不等式
解:上下限一般为键盘输入的a,b。
// 解不等式: a<1+1/(1+1/2)+...+1/(1+1/2+...+1/n)<b
#include <stdio.h>
#include<math.h>
void main()
{ long a,b,c,d,i;
double ts,s;
printf(" 请输入a,b: ");
scanf("%d,%d",&a,&b);
i=0;ts=0;s=0;
while(s<a)
{ i=i+1;
ts=ts+(double)1/i;
s=s+1/ts;
}
c=i;
while(s<b)
{i=i+1;
ts=ts+(double)1/i;
s=s+1/ts;
}
d=i-1;
printf("
满足不等式的正整数n为: %ld≤n≤%ld
",c,d);
}
2-2 韩信点兵
韩信在点兵的时候,为了知道有多少个兵,同时又能保住军事机密,便让士兵排队报数。
按从1至5报数,记下最末一个士兵报的数为1;
再按从1至6报数,记下最末一个士兵报的数为5;
再按1至7报数,记下最末一个报的数为4;
最后按1至11报数,最末一个士兵报的数为10。
你知道韩信至少有多少兵?
- 求解要点
设兵数为x,则x满足下述的同余方程组:
x=5y+1 即 x=1 (mod 5)
x=6z+5 x=5 (mod 6)
x=7u+4 x=4 (mod 7)
x=11v+10 x=10 (mod 11)
其中y,z,u,v都为正整数。试求满足以上方程组的最小正整数x。
应用枚举可得到至少的兵数。x从1开始递增1取值枚举当然可以,但不必要。事实上枚举次数可联系问题的具体实际大大缩减。
(1) 注意到x除11余10,于是可设置x从21开始,以步长11递增。此时,只要判别前三个条件即可。
(2) 由以上第2,4两方程知x+1为11的倍数,也为6的倍数。而11与6互素,因而x+1必为66的倍数。于是取x=65开始,以步长66递增。此时,只要判别x%5=1与x%7=4 两个条件即可。
这样可算得满足条件的最小整数x即点兵的数量。 - 程序实现
// 韩信点兵
include <stdio.h>
void main()
{ long int x;
x=65;
while(1)
{ x=x+66;
if(x%51 && x%74)
{ printf("至少有兵: %ld 个。",x);
break;
}
}
}
2-3 分解质因数
对给定区间[m,n]的正整数分解质因数,每一整数表示为质因数从小到大顺序的乘积形式。如果被分解的数本身是素数,则注明为素数。
例如, 2012=2*2*503, 2011=(素数!)。
解:对区间中的每一个整数i(b=i),用k(2——sqrt(i))试商:
若不能整除,说明该数k不是b的因数,k增1后继续试商。
若能整除,说明该数k是b的因数,打印输出"k*";b除以k的商赋给b(b=b/k)后继续用k试商(注意,可能有多个k因数),直至不能整除,k增1后继续试商。
按上述从小至大试商确定的因数显然为质因数。
如果有大于sqrt(n)的因数(至多一个!),在试商循环结束后要注意补上,不要遗失。
如果整个试商后b的值没有任何缩减,仍为原待分解数n,说明n是素数,作素数说明标记。
若k是b的因数,按格式输出,然后b=b/k后继续试商k。
若k不是b的因数,则k增1后继续。
若上述试商完成后1<b<i,说明i有一个大于sqrt(i)的因数,要补上该因数。
若试商后b还是原来的i,则i是素数。
// 质因数分解乘积形式
include"math.h"
include <stdio.h>
void main()
{long int b,i,k,m,n,w=0;
printf("[m,n]中整数分解质因数(乘积形式).
");
printf("请输入m,n:");
scanf("%ld,%ld",&m,&n);
for(i=m;i<=n;i++) // i为待分解的整数
{ printf("%ld=",i);
b=i;k=2;
while(k<=sqrt(i)) // k为试商因数
{if(b%k0)
{b=b/k;
if(b>1)
{printf("%ld*",k);
continue; // k为质因数,返回再试
}
if(b1) printf("%ld
",k);
}
k++;
}
if(b>1 && b<i)
printf("%ld
",b); // 输出大于i平方根的因数
if(b==i)
{printf("(素数!)
");w++;} // b=i,表示i无质因数
}
}
2-4 基于素数代数和的最大最小
定义和:
(和式中第k项±(2k-1)*(2k+1)的符号识别:当(2k-1)与(2k+1)中至少有一个素数,取“+”;其余取“-”。例如和式中第13项取“-”,即为-25*27。)
1) 求s(2011)。
2) 设1<=n<=2011,当n为多大时,s(n)最大。
3) 设1<=n<=2011,当n为多大时,s(n)最小。
解:代数和式中各项的符号并不是简单的正负相间,而是随着构成素数而改变。因而在求和之前应用“试商判别法”对第k个奇数2k-1是否为素数进行标注:
若2k-1为素数,标注a[k]=1;
否则,若2k-1不是素数,a[k]=0。
设置k循环(1——n),循环中分别情况求和:
若a[k]+a[k+1]>=1,即(2k-1)与(2k+1)中至少有一个素数,实施“+”;
否则,若a[k]+a[k+1]==0,即(2k-1)与(2k+1)中没有素数,实施“-”。
同时,设置最大值变量smax,最小值变量smin。
在循环中,每计算一个和值s,与smax比较确定最大值,同时记录此时的项数k1;与smin比较确定最小值,同时记录此时的项数k2。
// 基于素数的整数和
include<stdio.h>
include<math.h>
void main()
{ int t,j,n,k,k1,k2,a[3000]; long s,smax,smin;
printf(" 请输入整数n: ");
scanf("%d",&n);
for(k=1;k<=n+1;k++) a[k]=0;
for(k=2;k<=n+1;k++)
{for(t=0,j=3;j<=sqrt(2k-1);j+=2)
if((2k-1)%j0)
{t=1;break;}
if(t0) a[k]=1; // 标记第k个奇数2k-1为素数
}
s=3;smax=0;smin=s;
for(k=2;k<=n;k++)
{if(a[k]+a[k+1]>=1)
s+=(2k-1)(2k+1); // 实施代数和
else
s-=(2k-1)(2k+1);
if(s>smax){smax=s;k1=k;} // 比较求最大值smax
if(s<smin){smin=s;k2=k;} // 比较求最大值smin
}
printf("s(%d)=%ld
",n,s);
printf("当k=%d时s有最大值: %ld
",k1,smax);
printf("当k=%d时s有最小值: %ld
",k2,smin);
}
2-5 特定数字组成的平方数
用数字2,3,5,6,7,8,9可组成多少个没有重复数字的7位平方数?
解:求出最小7位数的平方根b, 最大7位数的平方根c.
用a枚举[b,c]中的所有整数,计算d=a*a,这样确保所求平方数在d中。
设置f数组统计d中各个数字的个数。如果f[3]=2,即平方数d中有2个“3”。
检测若f[k]>1(k=0——9),说明d中存在有重复数字,返回。
在不存在重复数字的情形下,检测若f[0]+f[1]+f[4]=0,说明7位平方数d中没有数字“0”,“1”,“4”,d满足题意要求,打印输出。
// 组成没有重复数字的7位平方数
include <math.h>
include <stdio.h>
void main()
{int k,m,n,t,f[10];
long a,b,c,d,w;
n=0;
b=sqrt(2356789);c=sqrt(9876532);
for(a=b;a<=c;a++)
{d=a*a; w=d; // 确保d为平方数
for(k=0;k<=9;k++) f[k]=0;
while(w>0)
{ m=w%10;f[m]++;w=w/10;}
for(t=0,k=1;k<=9;k++)
if(f[k]>1) t=1; // 测试三个平方数是否有重复数字
if(t0 && f[0]+f[1]+f[4]0) // 测试平方数中没有数字0,1,4
{n++;
printf(" %2d: ",n);
printf(" %ld=%ld^2
",d,a);
}
}
printf(" 共可组成%d个没有重复数字的7位平方数.
",n);
}
2-6 写出例2-2中对称方阵的完整程序,并运行程序。
对称方阵程序:
include <stdio.h>
void main()
{int i,j,n,a[30][30];
printf(" 请确定方阵阶数n: ");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{if(i+j<=n+1 && i<=j)
a[i][j]=(n+1)/2-i+1; // 方阵上部元素赋值
if(i+j<n+1 && i>j)
a[i][j]=(n+1)/2-j+1; // 方阵左部元素赋值
if(i+j>=n+1 && i>=j)
a[i][j]=i-n/2; // 方阵下部元素赋值
if(i+j>n+1 && i<j)
a[i][j]=j-n/2; // 方阵右部元素赋值
}
printf(" %d阶对称方阵为:
",n);
for(i=1;i<=n;i++)
{ for(j=1;j<=n;j++) // 输出对称方阵
printf("%3d",a[i][j]);
printf("
");
}
}
2-7 四则运算式
把数字1,2,...,9这9个数字填入以下含加减乘除的综合运算式中的9个□中,使得该式成立
□□×□+□□□÷□-□□=0
要求数字1,2,...,9这9个数字在各式中都出现一次且只出现一次,且约定数字“1”不出现在数式的一位数中(即排除各式中的各个1位数为1这一平凡情形)。
(1) 求解要点
设式右的5个整数从左至右分别为a,b,c,d,e,其中a,e为二位整数,b,d为大于1的一位整数,c为三位整数。设置a,b,c,d循环,对每一组a,b,c,d,计算e=a*b+c/d。若其中的c/d非整数,或所得e非二位数,则返回。
然后分别对5个整数进行数字分离,设置f数组对5个整数分离的共9个数字进行统计,f(x)即为数字x(1—9)的个数。
若某一f(x)不为1,不满足数字1,2,...,9这九个数字都出现一次且只出现一次,标记t=1.
若所有f(x)全为1,满足数字1,2,...,9这九个数字都出现一次且只出现一次,保持标记t=0, 则输出所得的完美综合运算式。
设置n统计解的个数。
(2) 程序实现
// 四则运算式
include <stdio.h>
void main()
{int x,y,t,k,a,b,c,d,e,n=0;
int m[6],f[11];
for(a=12;a<=98;a++)
for(b=2;b<=9;b++)
for(c=123;c<=987;c++) // 对a,b,c,d 实施枚举
for(d=2;d<=9;d++)
{x=c/d;e=ab+x;
if(c!=xd || e>100) continue;
m[1]=a;m[2]=c;m[3]=e;m[4]=b;m[5]=d;
for(x=0;x<=9;x++) f[x]=0;
for(k=1;k<=5;k++)
{y=m[k];
while(y>0)
{x=y%10;f[x]=f[x]+1;
y=(y-x)/10; // 分离数字f数组统计
}
}
for(t=0,x=1;x<=9;x++)
if(f[x]!=1)
{t=1; break;} // 检验数字0--9各只出现一次
if(t==0) // 输出一个解,用n统计个数
{n++;
printf("%2d: %2d*%1d+%3d/%1d-%2d=0
",n,a,b,c,d,e);
}
}
printf(" n=%d.
",n);
}
2-8 合数世纪探求
定义一个世纪的100个年号中不存在一个素数,即100个年号全为合数的世纪称为合数世纪。
探索最早的合数世纪。
(1) 设计要点
应用穷举搜索,设置a世纪的的50个奇数年号(偶数年号无疑均为合数)为b,用k试商判别b是否为素数,用变量s统计这50个奇数中的合数的个数。
对于a世纪,若s=50,即50个奇数都为合数,找到a世纪为最早的合数世纪,打印输出后退出循环结束。
(2) 合数世纪程序设计
// 合数世纪探求
include <stdio.h>
include <math.h>
void main()
{long a,b,k; int s,x;
a=1;
while (1)
{a++;s=0; // 检验a世纪
for(b=a100-99;b<=a100-1;b+=2) // 穷举a世纪奇数年号b
{x=0;
for(k=3;k<=sqrt(b);k+=2)
if(b%k0)
{x=1;break;}
if(x0)break; // 当前为非合数世纪时,跳出循环进行下世纪的探求
s=s+x; // 年号b为合数时,x=1,s增1
}
if(s==50) // s=50,即50个奇数均为合数
{ printf("最早出现的合数世纪为 %ld 世纪!
",a);
break;
}
}
}
2-9 最小连续n个合数
试求出最小的连续n个合数。(其中n是键盘输入的任意正整数。)
(1)设计要点
求出区间[c,d]内的所有素数(区间起始数c可由小到大递增),检验其中每相邻两素数之差。若某相邻的两素数m,f之差大于n,即m-f>n,则区间[f+1,f+n]中的n个数为最小的连续n个合数。
应用试商法求指定区间[c,d](约定起始数c=3,d=c+10000)上的所有素数。求出该区间内的一个素数m,设前一个素数为f,判别:
若m-f>n,则输出结果[f+1,f+n]后结束;
否则,作赋值f=m,为求下一个素数作准备。
如果在区间[c,d]中没有满足条件的解,则作赋值:c=d+2,d=c+10000,继续试商下去,直到找出所要求的解。
(2) 程序实现
// 求最小的连续n个合数
include <stdio.h>
include <math.h>
void main()
{ long c,d,f,m,j;
int t,n;
printf(" 求最小的n个连续合数.
");
printf(" 请输入n:");
scanf("%d",&n);
c=3;d=c+10000;
f=3;
while(1)
{ for(m=c;m<=d;m+=2)
{ for(t=0,j=3;j<=sqrt(m);j+=2)
if(m%j0) // 实施试商
{t=1;break;}
if(t0 && m-f>n) // 满足条件即行输出
{ printf("最小的%d个连续合数区间为:",n);
printf("[%ld,%ld]。
",f+1,f+n);
getch();return;
}
if(t==0) f=m; // 每求出一个素数m后赋值给f
}
if(m>d)
{c=d+2;d=c+10000;} // 每一轮试商后改变c,d转下一轮
}
}
2-10 和积9数字三角形
求解和为给定的正整数s(s≥45)的9个互不相等的正整数填入9数字三角形,使三角形三边上的4个数字之和相等(s1)且三边上的4个数字之积也相等(s2)。
图2-7 9数字三角形
(1)求解要点。
把和为s的9个正整数存储于b数组b(1),…,b(9)中,分布如下图所示。为避免重复,不妨约定三角形中数字“下小上大、左小右大”,即b(1)<b(7)<b(4)且b(2)<b(3)且b(6)<b(5)且b(9)<b(8)。
图2-8 b数组分布示意图
可以根据约定对b(1)、b(7)和b(4)的值进行循环探索,设置:
b(1)的取值范围为1~(s-21)/3(因其他6个数之和至少为21)。
b(7)的取值范围为b(1)+1~(s-28)/2。
b(4)的取值范围为b(7)+1~(s-36)。
同时探索判断步骤如下:
1)若(s+b(1)+b(7)+b(4))%3≠0,则继续探索;否则,记s1=(s+b(1)+b(7)+b(4))/3。
2)根据约定对b(3)、b(5)和b(8)的值进行探索,设置:
b(3)的取值范围为(s1-b(1)-b(4))/2+1~s1-b(1)-b(4)。
b(5)的取值范围为(s1-b(4)-b(7))/2+1~s1-b(4)-b(7)。
b(8)的取值范围为(s1-b(1)-b(7))/2+1~s1-b(1)-b(7))。
同时根据各边之和为s1,计算出b(2)、b(6)和b(9):
b(2)=s1-b(1)-b(4)-b(3)
b(6)=s1-b(4)-b(5)-b(7)
b(9)=s1-b(1)-b(7)-b(8)
3)若b数组存在相同正整数,则继续探索。
4)设s2=b(1)*b(2)*b(3)*b(4),若另两边之积不为s2,则继续探索;否则探索成功,打印输出结果,接着继续探索直到所有数字组探索完毕为止。
(2)9数字三角形求解程序设计。
// 9数字三角形求解
include<stdio.h>
include<math.h>
void main()
{
int k,j,t,s,s1,s2,n,b[10];
printf(" 请输入正整数s:");
scanf("%d",&s);
n=0;
for(b[1]=1;b[1]<=(s-21)/3;b[1]++)
for(b[7]=b[1]+1;b[7]<=(s-28)/2;b[7]++)
for(b[4]=b[7]+1;b[4]<=s-36;b[4]++)
{
if((s+b[1]+b[4]+b[7])%3!=0) continue;
s1=(s+b[1]+b[4]+b[7])/3;
for(b[3]=(s1-b[1]-b[4])/2+1;b[3]<s1-b[1]-b[4];b[3]++)
for(b[5]=(s1-b[4]-b[7])/2+1;b[5]<s1-b[4]-b[7];b[5]++)
for(b[8]=(s1-b[1]-b[7])/2+1;b[8]<s1-b[1]-b[7];b[8]++)
{
b[2]=s1-b[1]-b[4]-b[3];
b[6]=s1-b[4]-b[7]-b[5];
b[9]=s1-b[1]-b[7]-b[8];
t=0;
for(k=1;k<=8;k++)
for(j=k+1;j<=9;j++)
if(b[k]b[j]) {t=1;k=8;break;}
if(t1) continue;
s2=b[1]b[2]b[3]b[4];
if(b[4]b[5]b[6]b[7]!=s2) continue;
if(b[1]b[9]b[8]*b[7]!=s2) continue;
n++;
printf(" %3d:%2d",n,b[1]);
for(k=2;k<=9;k++)
printf(", %2d",b[k]);
printf(" s1=%d, s2=%d
",s1,s2);
}
}
printf("共%d个解。",n);
}
习题3
3-1 递推求解b数列
已知b数列定义:
递推求b数列的第20项与前20项之和。
解:
include <stdio.h>
void main()
{ int k,n; long b[3000],s;
printf(" 请输入n: ");
scanf("%d",&n);
b[1]=1;b[2]=2;s=3;
for(k=3;k<=n;k++)
{ b[k]=3*b[k-1]-b[k-2];
s+=b[k];
}
printf(" b(%d)=%ld
",n,b[n]);
printf(" s=%ld
",s);
}
3-2 双关系递推数列
集合M定义如下:
1)
2)
3)再无别的数属于M
试求集合M元素从小到大排列的第2011个元素与前2011 个元素之和。
解:(1)设计要点
设n个数在数组m中,2x+1与3x+1均作为一个队列,从两队列中选一排头(数值较小者)送入数组m中。所谓“排头”就是队列中尚未选入m的最小的数(下标)。这里用p2表示2x+1这一列的排头的下标,用p3表示3x+1这一列的排头的下标。
if(2*m(p2)<3*m(p3))
{ m(i)=2*m(p2)+1;p2++;}
if(2*m(p2)>3*m(p3))
{ m(i)=3*m(p3)+1;p3++;}
特别注意:两队列若出现相等时,给m数组赋值后,两排头都要增1。
if(2*m(p2)==3*m(p3))
{ m(i)=2*m(p2)+1;
p2++; p3++; // 为避免重复项,P2,p3均须增1
}
(2) 程序设计
// 双关系递推
include <stdio.h>
void main()
{int n,p2,p3,i;long s,m[3000];
m[1]=1;s=1;
p2=1;p3=1; // 排头p2,p3赋初值
printf(" 请输入n: ");
scanf("%d",&n);
for(i=2;i<=n;i++)
if(2m[p2]❤️m[p3])
{ m[i]=2m[p2]+1; s+=m[i];
p2++;
}
else
{ m[i]=3m[p3]+1; s+=m[i];
if(2m[p2]==3m[p3]) p2++; // 为避免重复项,P2须增1
p3++;
}
printf(" m(%d)=%ld
",n,m[n]);
printf(" s=%ld
",s);
}
3-3 多幂序列
设x,y,z为非负整数,试计算集合
的元素由小到大排列的多幂序列第n项与前n项之和。
(1)递推算法设计
集合由2的幂、3的幂与5的幂组成,实际上给出的是3个递推关系。
显然,第1项也是最小项为1(当x=y=z=0时)。
从第2项开始,为了实现从小到大排列,设置3个变量a,b,c,a为2的幂,b为3的幂,c为5的幂,显然a,b,c互不相等。
设置k循环(k=2,3,…,n,其中n为键盘输入整数),在k循环外赋初值:a=2;b=3;c=5;s=1;在k循环中通过比较赋值:
当a<b且a<c时,由赋值f[k]=a确定为序列的第k项;然后a=a*2,即a按递推规律乘2,为后一轮比较作准备;
当b<a且b<c时,由赋值f[k]=b确定为序列的第k项;然后b=b*3,即b按递推规律乘3,为后一轮比较作准备。
当c<a且c<b时,由赋值f[k]=c确定为序列的第k项;然后c=c*5,即c按递推规律乘5,为后一轮比较作准备。
递推过程描述:
a=2;b=3;c=5; // 为递推变量a,b,c赋初值
for(k=2;k<=n;k++)
{ if(a<b && a<c)
{ f[k]=a;a=a*2;} // 用a给f[k]赋值
else if(b<a && b<c)
{ f[k]=b;b=b*3;} // 用b给f[k]赋值
else
{ f[k]=c;c=c*5;} // 用c给f[k]赋值
}
在这一算法中,变量a,b,c是变化的,分别代表2的幂、3的幂与5的幂。
上述递推算法的时间复杂度与空间复杂度均为O(n)。
(2)多幂序列程序实现
// 多幂序列求解
include <stdio.h>
void main()
{int k,m,t,p2,p3,p5;
double a,b,c,s,f[100];
printf(" 求数列的第m项与前m项和,请输入m: ");
scanf("%d",&m);
f[1]=1;p2=0;p3=0;p5=0;
a=2;b=3;c=5;s=1;
for(k=2;k<=m;k++)
{ if(a<b && a<c)
{ f[k]=a;a=a2; // 用2的幂给f[k]赋值
t=2;p2++; // t=2表示2的幂,p2为指数
}
else if(b<a && b<c)
{ f[k]=b;b=b3; // 用3的幂给f[k]赋值
t=3;p3++; // t=3表示3的幂,p3为指数
}
else
{ f[k]=c;c=c*5; // 用5的幂给f[k]赋值
t=3;p5++; // t=5表示5的幂,p5为指数
}
s+=f[k];
}
printf(" 数列的第%d项为: %.0f ",m,f[m]);
if(t2) // 对输出项进行标注
printf("(2^%d)
",p2);
else if(t3)
printf("(3^%d)
",p3);
else
printf("(5^%d)
",p5);
printf(" 数列的前%d项之和为:%.0f
",m,s);
}
3-4 双幂积序列的和
由集合 元素组成的复合幂序列,求复合幂序列的指数和x+y≤n(正整数n从键盘输入)的各项之和
(1)设计要点
归纳求和递推关系:
当x+y=0时,s(1)=1;
当x+y=1时,s(1)=2+3;
当x+y=2时,s(2)=22+2×3+32=2*s(1)+ 32
当x+y=3时,s(3)=23+22×3+2×32+33=2*s(2)+ 33
一般地,当x+y=k时,s(k)=2*s(k−1)+3k
即有递推关系:
s(k)=2*s(k)+3k
其中3k可以通过变量迭代实现。这样可以省略数组,简化为一重循环实现复合幂序列求和。
(2)程序实现
// 复合幂序列求和
include <stdio.h>
void main()
{int k,n; long sum,t,s[100];
printf("请输入幂指数和至多为n:");
scanf("%d",&n);
t=1;s[0]=1; sum=1;
for(k=1;k<=n;k++)
{t=t3; // 迭代得t=3^k
s[k]=2s[k-1]+t; // 实施递推
sum=sum+s[k];
}
printf("幂指数和至多为%d的幂序列之和为:%ld
",n,sum);
}
3-5 粒子裂变
核反应堆中有α和β两种粒子,每秒钟内一个α粒子可以裂变为3个β粒子,而一个β粒子可以裂变为1个α粒子和2个β粒子。若在t=0时刻的反应堆中只有一个α粒子,求在t秒时反应堆裂变产生的α粒子和β粒子数。
1. 算法设计
设在t秒时α粒子数为f(t),β粒子数为g(t),依题可知:
g(t)=3f(t-1)+2g(t-1) (1)
f(t)=g(t-1) (2)
g(0)=0,f(0)=1
由(2)得f(t-1)=g(t-2) (3)
将式(3)代入(1)得
g(t)=2g(t-1)+3g(t-2) (t≥2) (4)
g(0)=0,g(1)=3 (5)
以递推关系(4)与初始条件(5)完成递推。
2.粒子裂变C程序设计
// 粒子裂变
include<stdio.h>
void main()
{int t,k;long g[100];
printf(" input t:");
scanf("%d",&t);
g[0]=0; g[1]=3; // 确定初始条件
for(k=2;k<=t;k++)
g[k]=2g[k-1]+3g[k-2]; // 完成递推
printf("%d 秒时反应堆中β粒子数为:%ld
",t,g[t]);
printf("%d 秒时反应堆中α粒子数为:%ld
",t,g[t-1]);
}
3-6 m行n列逆转矩阵
图3-4所示为4行5 列逆转矩阵。
试应用递推设计构造并输出任意指定m行n列逆转矩阵。
解: 对输入的m,n,取c=min(m,n),计算数字矩阵的圈数d=(c+1)/2。
设置i(1——d)循环,从外圈至内圈,分4边进行递推赋值。
程序设计:'
// m×n数字逆转矩阵
#include <stdio.h>
void main()
{int i,j,c,d,h,v,m,n,s,a[30][30];
printf(" m行n列矩阵,请确定m,n: "); scanf("%d,%d",&m,&n);
c=n;
if(m<n) c=m;
d=(c+1)/2;
s=0;v=0;
for(i=1;i<=d;i++) // 从外至内第d圈赋值
{ v++;
for(h=i;h<=m-i;h++) // 一圈的左列从上至下递增
{ s++; a[h][v]=s;}
for(v=i;v<=n-i;v++) // 一圈的下行从左至右递增
{ s++; a[h][v]=s;}
for(h=m+1-i;h>i;h--) // 一圈的右列从下至上递增
{ s++; a[h][v]=s;
if(s==m*n) {h=i;break;}
}
for(v=n+1-i;v>i;v--) // 一圈的上行从右至左递增
{ s++; a[h][v]=s;
if(s==m*n) {v=i;break;}
}
}
printf(" %d行%d列旋转矩阵为:
",m,n);
for(i=1;i<=m;i++)
{ for(j=1;j<=n;j++) // 按m行n列输出矩阵
printf("%4d",a[i][j]);
printf("
");
}
}
3-7 猴子吃桃
有一猴子第1天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了1个。第2天早上又将剩下的桃子吃掉一半,又多吃了1个。以后每天早上都吃了前一天剩下的一半后又多吃1个。到第10天早上想再吃时,见只剩下1个桃子了。
求第1天共摘了多少个桃子。
(1) 求解要点
第1天的桃子数是第2天桃子数加1后的2倍,第2天的桃子数是第3天桃子数加1后的2倍,…,一般地,第k天的桃子数是第k+1天桃子数加1后的2倍。设第k天的桃子数是t(k),则有递推关系
t(k)=2*(t(k+1)+1) (k=1,2,…,9)
初始条件:t(10)=1
逆推求出t(1),即为所求的第一天所摘桃子数。
(2) 程序设计
// 猴子吃桃程序 '
#include <stdio.h>
void main()
{ int k; long t[1000];
t[10]=1; // 确定初始条件
for(k=9;k>=1;k--) // 逆推计算t(1)
t[k]=2*(t[k+1]+1);
printf(" 第 1 天摘桃%ld个。
",t[1]);
for(k=1;k<=9;k++)
{ printf(" 第 %d 天面临%4ld个桃,",k,t[k]);
printf(" 吃了%4ld+1=%4ld个,",t[k]/2,t[k]/2+1);
printf(" 还剩%4ld个。
",t[k]/2-1);
}
printf(" 第10天早上还剩1个。");
}
3-8 拓广猴子吃桃
有一猴子第1天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了m个。第2天早上又将剩下的桃子吃掉一半,又多吃了m个。以后每天早上都吃了前一天剩下的一半后又多吃m个。到第n天早上想再吃时,见只剩下d个桃子了。
求第1天共摘了多少个桃子(m,n,d由键盘输入)?
解:递推关系
t(k)=2*(t(k+1)+m) (k=1,2,…,n-1)
初始条件:t(n)=d
逆推求出t(1),即为所求的第一天所摘桃子数。
// 拓广猴子吃桃程序
#include <stdio.h>
void main()
{ int d,k,m,n; long t[1000];
printf(" 请确定正整数m,n,d: ");
scanf("%d,%d,%d",&m,&n,&d);
t[n]=d; // 确定初始条件
for(k=n-1;k>=1;k--) // 逆推计算t(1)
t[k]=2*(t[k+1]+m);
printf(" 第 1 天摘桃%ld个。
",t[1]);
for(k=1;k<=n-1;k++)
{ printf(" 第 %d 天面临%4ld个桃,",k,t[k]);
printf(" 吃了%4ld+%d=%4ld个,",t[k]/2,m,t[k]/2+1);
printf(" 还剩%4ld个。
",t[k]/2-m);
}
printf(" 第%d天早上还剩%d个。",n,d);
}
3-9 据例3-1中求裴波那契数列的第40项与前40项之和的递推算法与迭代算法,写出完整的程序,并比较其运行结果。
(1) 应用递推求解
// 裴波那契数列递推程序
#include <stdio.h>
void main()
{ int k; long s,f[50];
f[1]=1;f[2]=1;
s=f[1]+f[2]; // 数组元素与和变量赋初值
for(k=3;k<=40;k++)
{ f[k]=f[k−1]+f[k−2]; // 实施递推
s+=f[k]; // 实施求和
}
printf(" f数列第40项为: %ld
",f[n]);
printf(" 前40项之和为: %ld
",s);
}
(2) 应用迭代求解
// 裴波那契数列迭代程序
#include<stdio.h>
void main()
{ int k; long a,b,s;
a=1;b=1;s=a+b; // 迭代变量a,b,s赋初值
k=2;
while(k<=20) // 控制迭代次数
{ a=a+b; // 推出a是f数列的第2k-1项
b=a+b; // 推出b是f数列的第2k项
s=s+a+b; // 推出s是f数列的前2k项之和
k=k+1;
}
printf(" f数列的第40项为:%ld
",b);
printf(" 前40项之和为:%ld
",s);
}
习题4
4-1 阶乘的递归求解
阶乘n!定义: n!=1(n=1);n!=n*(n-1)! (n>1)
设计求n!的递归函数,调用该函数求
解: 定义n!的递归函数f(n),在求和的k(1——n)循环中实施求和
s+=(double)1/f(k);
程序设计:
#include <stdio.h>
long f(int n)
{ long g;
if(n==1) g=1;
else g=n*f(n-1);
return(g);
}
void main()
{ int k,n;
double s=1;
printf(" 请输入n: ");scanf("%d",&n);
for(k=1;k<=n;k++)
s+=(double)1/f(k);
printf(" s=%f
",s);
}
4-2 递归求解f数列
已知f数列定义:
建立f数列的递归函数,求f数列的第n项与前n项之和。
解:定义f数列的递归函数f(n),在求和的k(1——n)循环中实施求和 s+=f(k)。
程序设计:
#include <stdio.h>
long f(int n)
{ long g;
if(n==1 || n==2) g=1;
else g=f(n-1)+f(n-2);
return(g);
}
void main()
{ int k,n; long s=0;
printf(" 请输入n: ");scanf("%d",&n);
for(k=1;k<=n;k++)
s+=f(k);
printf(" f(%d)=%ld
",n,f(n));
printf(" s=%ld
",s);
}
4-3 递归求解b数列
已知b数列定义:
建立b数列的递归函数,求b数列的第n项与前n项之和。
解:#include <stdio.h>
long b(int n)
{ long g;
if(n==1) g=1;
else if(n==2) g=2;
else g=3*b(n-1)-2*b(n-2);
return(g);
}
void main()
{ int k,n; long s=0;
printf(" 请输入n: ");scanf("%d",&n);
for(k=1;k<=n;k++)
s+=b(k);
printf(" b(%d)=%ld
",n,b(n));
printf(" s=%ld
",s);
}
4-4 递归求解双递推摆动数列
已知递推数列:a(1)=1,a(2i)=a(i)+1,a(2i+1)=a(i)+a(i+1),(i为正整数),试建立递归,求该数列的第n项与前n项的和。
// 摆动数列
#include <stdio.h>
int a(int n)
{ int g;
if(n==1) g=1;
else if(n%2==0) g=a(n/2)+1;
else g=a((n-1)/2)+a((n+1)/2);
return(g);
}
void main()
{ int k,n; long s=0;
printf(" 请输入n: ");scanf("%d",&n);
for(k=1;k<=n;k++)
s+=a(k);
printf(" a(%d)=%d
",n,a(n));
printf(" s=%ld
",s);
}
4-5 应用递归设计输出杨辉三角。
// 杨辉三角递归设计
void c(int a[],int n)
{int i;
if(n==0) a[1]=1;
else if(n==1)
{a[1]=1;a[2]=1;}
else
{c(a,n-1);
a[n+1]=1;
for(i=n;i>=2;i--)
a[i]=a[i]+a[i-1];
a[1]=1;
}
}
#include<stdio.h>
void main()
{ int i,j,k,n,a[100];
printf(" 请输入杨辉三角的行数:");
scanf("%d",&n);
for(j=0;j<=n;j++)
{c(a,j);
for(k=1;k<=30-2*j;k++) printf(" ");
for(i=1;i<=j;i++)
printf("%4d",a[i]);
printf("%4d
",1);
}
}
4-6 试把m×n顺转矩阵的递归设计转变为递推设计。
解: 对输入的m,n,取c=min(m,n),计算数字矩阵的圈数d=(c+1)/2。
设置i(1——d)循环,从外圈至内圈,分4边进行递推赋值。
程序设计:
// m×n数字旋转矩阵
#include <math.h>
#include <stdio.h>
void main()
{int i,j,c,d,h,v,m,n,s,a[30][30];
printf(" m行n列矩阵,请确定m,n: "); scanf("%d,%d",&m,&n);
c=n;
if(m<n) c=m;
d=(c+1)/2;
s=0;h=0;
for(i=1;i<=d;i++) // 从外至内第d圈赋值
{h++;
for(v=i;v<=n-i;v++)
{s++;a[h][v]=s;} // d圈的首行从左至右赋值
for(h=i;h<=m-i;h++)
{s++;a[h][v]=s;} // d圈的尾列从上至下赋值
for(v=n+1-i;v>=i+1;v--)
{ s++;a[h][v]=s; // d圈的尾行从右至左赋值
if(s==m*n) {i=d;break;}
} // 赋值完成即行退出
for(h=m+1-i;h>=i+1;h--)
{ s++;a[h][v]=s; // d圈的首列从下至上赋值
if(s==m*n) {i=d;break;}
}
}
printf(" %d行%d列旋转矩阵为:
",m,n);
for(i=1;i<=m;i++)
{ for(j=1;j<=n;j++) // 按m行n列输出矩阵
printf("%4d",a[i][j]);
printf("
");
}
}
4-7 试应用递归设计构造并输出任意指定逆转m×n矩阵。
解:在递归函数中,每圈4边按左列左列从上至下递增、下行从左至右递增、右列从下至上递增、上行从右至左递增给元素赋值。
程序设计:
// m×n逆转矩阵递归设计
#include <stdio.h>
int m,n,a[20][20]={0};
void main()
{ int h,v,b,s,d;
printf(" 数阵为m行n列,请确定m,n:");
scanf("%d,%d",&m,&n);
s=m;
if(m>n) s=n;
b=1;d=1;
void t(int b,int s,int d); // 递归函数说明
t(b,s,d); // 调用递归函数
printf(" %d×%d逆转矩阵:
",m,n);
for(h=1;h<=m;h++)
{for(v=1;v<=n;v++)
printf(" %3d",a[h][v]);
printf("
");
}
return;
}
void t(int b,int s,int d) // 定义递归函数
{ int j,h=b,v=b;
if(s<=0) return; // 递归出口
for(j=1;j<=m+1-2*b;j++) // 一圈的左列从上至下递增
{ a[h][v]=d;h++;d++;}
for(j=1;j<=n+1-2*b;j++) // 一圈的下行从左至右递增
{ a[h][v]=d;v++;d++;}
for(j=1;j<=m+1-2*b;j++) // 一圈的右列从下至上递增
{ a[h][v]=d;h--;d++;
if(d>m*n) return;
}
for(j=1;j<=n+1-2*b;j++) // 一圈的上行从右至左递增
{ a[h][v]=d;v--;d++;
if(d>m*n) return; // 另一递归出口
}
t(b+1,s-2,d); // 调用内一圈递归函数
}
4-8 应用递归设计实现n个相同元素与另m个相同元素的所有排列。
解: 设置递归函数p(k),1≤k≤m+n,元素a[k]取值为0或1。
当k=m+n时,作变量h统计“0”的个数。若h=m则打印输出一排列,并用s统计排列个数。然后回溯返回,继续。
当k<m+n时,还不足n+m个数,则调用p(k+1)探索下一个数。
主程序中调用p(1)。
// n个1与另m个0的排列
#include <stdio.h>
int m,n,r,a[30]; long s=0;
void main()
{ int p(int k);
printf(" input n,m: "); scanf("%d,%d",&n,&m);
printf(" %d个1与%d个0的排列:
",n,m);
p(1); // 从第1个数开始
printf("
s=%ld
",s); // 输出排列的个数
}
// 排列递归函数
#include <stdio.h>
int p(int k)
{ int h,i,j;
if(k<=m+n)
{ for(i=0;i<=1;i++)
{ a[k]=i; // 探索第k个数赋值i
if(k==m+n) // 若已到m+n个数则检测0的个数h
{ for(h=0,j=1;j<=n+m;j++)
if(a[j]==0) h++;
if(h==m) // 若0的个数为m个,输出一排列
{ s++; printf(" ");
for(j=1;j<=n+m;j++)
printf("%d",a[j]);
if(s%10==0) printf("
");
}
}
else
p(k+1); // 若没到n+m个数,则调用p(k+1)探索下一个数
}
}
return s;
}
习题5
5-1 倒桥本分数式
把1,2,...,9这9个数字填入下式的9个方格中,数字不得重复,且要求1不得填在各分数的分母,且式中各分数的分子分母没有大于1的公因数,使下面的分数等式成立
这一填数分数等式共有多少个解?
解: 在桥本分数式回溯程序中修改
// 倒桥本分数式回溯实现
// 把1,2,...,9填入□□/□+□□/□=□□/□
#include <stdio.h>
void main()
{int g,i,k,u,t,a[10];
long m1,m2,m3;
i=1;a[1]=1;
while (1)
{g=1;
for(k=i-1;k>=1;k--)
if(a[i]==a[k]) {g=0;break;} // 两数相同,标记g=0
if(i==9 && g==1 && a[1]<a[4] && a[1]>1 && a[7]>1)
{m1=a[2]*10+a[3];
m2=a[5]*10+a[6];
m3=a[8]*10+a[9];
for(t=0,u=2;u<=9;u++)
{if(a[1]%u==0 && m1%u==0) t=1;
if(a[4]%u==0 && m2%u==0) t=1;
if(a[7]%u==0 && m3%u==0) t=1;
}
if(t==0 && m1*a[4]*a[7]+m2*a[1]*a[7]==m3*a[1]*a[4]) // 判断等式
{printf(" %d/%ld+%d/%ld",m1,a[1],m2,a[4]);
printf("=%d/%ld
",m3,a[7]);
}
}
if(i<9 && g==1)
{i++;a[i]=1;continue;} // 不到9个数,往后继续
while(a[i]==9 && i>1) i--; // 往前回溯
if(a[i]==9 && i==1) break;
else a[i]++; // 至第1个数为9结束
}
}
5-2 两组均分
参加拔禾比赛的12个同学的体重如下:
48,43,57,64,50,52,18,34,39,56,16,61
为使比赛公平,要求参赛的两组每组6个人,且每组同学的体重之和相等。
请设计算法解决这 “两组均分”问题。
(1) 求解要点
一般地,对已知的2n(n从键盘输入)个整数,确定这些数能否分成2个组,每组n个数,且每组数据的和相等。
我们可采用回溯法逐步实施调整。
对于已有的存储在b数组的2n个数,求出总和s与其和的一半s1(若这2n个数的和s为奇数,显然无法分组)。把这2n个数分成二个组,每组n个数。为方便调整,设置数组a存储b数组的下标值,即a(i):1─2n。
考察b(1)所在的组,只要另从b(2)─b(2n)中选取n-1个数。即定下a(1)=1,其余的a(i)(i=2,…,n)在2─2n中取不重复的数。因组合与顺序无关,不妨设
2 ≤ a(2)<a(3)<...<a(n) ≤2n
从a(2)取2开始,以后a(i)从a(i-1)+1开始递增1取值,直至n+i为止。这样可避免重复。
当a(n)已取值,计算s=b(1)+b(a(2))+…+b(a(n)),对和s进行判别:
若s=s1,满足要求,实现平分。
若s≠s1,则a(n)继续增1再试。如果a(n)已增至2n,则回溯前一个a(n-1)增1再试。如果a(n-1)已增至2n-1,继续回溯。直至a(2)增至n+2时,结束。
二堆均分问题并不总有解。有解时,找到并输出所有解。没有解时,显示相关提示信息“无法实现平分”。
(2) 两组均分程序设计
// 两组均分程序设计
#define N 50
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main()
{int n,m,a[N],b[2*N],i,j,t;
long s1,s=0;
printf("把2n个整数分为和相等的两个组,每组n个数.
");
t=time(0)%1000;srand(t); // 随机数发生器初始化
printf(" input n :"); scanf("%d",&n);
for(s=0,i=1;i<=2*n;i++) // 产生2n个不同的随机整数
{t=0;b[i]=rand()%(5*n)+10;
for(j=1;j<=i-1;j++)
if(b[i]==b[j])
{t=1;break;}
if(t==1) {i--;continue;} // 出现相同数时,返回重新产生
s+=b[i]; printf("%d ",b[i]);
}
if(s%2==0)
{printf("
以上%d个整数总和为%d.
",2*n,s);
s1=s/2;
}
else
printf("
和%ld为奇数,无法平分!
",s);
a[1]=1;i=2;a[i]=2;
m=0;
while(1)
{if(i==n)
{for(s=0,j=1;j<=n;j++)
s+=b[a[j]];
if(s==s1) // 满足均分条件时输出
{m++; printf("NO%d: ",m);
for(j=1;j<=n;j++)
printf("%d ",b[a[j]]);
}
}
else
{i++; a[i]=a[i-1]+1; continue;}
while(a[i]==n+i) i--; // 调整或回溯
if(i>1) a[i]++;
else break;
}
if(m>0) printf("共有以上%d种分法。",m);
else printf(" 无法实现二堆均分. ");
}
5-3 指定低逐位整除数探求
试求出所有最高位为3的24位低逐位整除数(除个位数字为“0”外,其余各位数字均不得为“0”)。
// 最高位为3的n位右逐位整除
#include<stdio.h>
void main()
{ int i,j,n,r,t,a[100]; long s=0;
printf(" 逐位整除n位,请确定n:");
scanf("%d",&n);
printf(" 所求%d位最高位为3的右逐位整除数:
",n);
for(j=1;j<=100;j++) a[j]=1;
t=0;a[1]=0;i=1;
while(a[1]<1)
{ if(t==0 && i<n) i++;
for(r=0,j=i;j>=1;j--) // 检测i时是否整除i
{ r=r*10+a[j]; r=r%i; }
if(r!=0)
{ a[i]=a[i]+1;t=1; // 余数r!=0时a[i]增1,t=1
while(a[i]>9 && i>1)
{ a[i]=1;i--; // 回溯
a[i]=a[i]+1;
}
}
else t=0; // 余数r=0时,t=0
if(t==0 && i==n)
{ if(a[n]==3)
{s++;printf(" %ld: ",s);
for(j=n;j>=1;j--)
printf("%d",a[j]);
printf("
");
}
a[i]=a[i]+1;
}
}
if(s>0)
printf(" 最高位为3的%d位右逐位整除数共%ld个.",n,s);
else
printf(" 未找到n位右逐位整除数.",n);
}
5-4 枚举求解8项素数和环,与回溯结果进行比较。
(1) 设计要点
为简化输出,环序列简化为一般序列输出,为避免重复,约定首项为“1”。
环中的每一项为一个数字,相连的8项构成一个8位数。因而设置a循环在没有重复数字数字且以“1”开头的8位数812345678——18765432中枚举。注意到所有1——8没有重复数字的8位数的数字和为9的倍数,该数也为9的倍数,为此,枚举循环步长可取9,以精简枚举次数。
为操作与判断方便,设置3个数组:
f数组统计8位数a中各个数字的频数。如f[3]=2,即a中有2个数字“3”。
g数组表示8位数a中每位数的数字。如g[4]=6,即a的从高位开始第4位数为数字“6”。
b数组标记整数x是否为素数。如b[13]=1,标识“1”表示13为素数,标识“0”为非素数。
枚举实施:
1) 注意到8项中每相邻两项之和不超过15,对15以内的5个素数用b数组标注“1”,其余均为“0”。
2) 在8位数的a 循环中,对a实施8次求余分离出各个数字x,应用f[x]++统计数字x的频数,应用g[9-k]=x记录a的各位数字。
3) 设置k(1——8)判断循环:
若f[k]!=1 ,表明数字k出现重复或遗漏,返回。
若 b[g[k]+g[k+1]]!=1,表明相邻的第k项与第k+1项之和不是素数,返回。顺便说明,为判断方便,首项“1”先行赋值给g[9],以与g[8]相邻,在k循环中一道进行判别。
4) 通过以上判断筛选的a,其各个数字即为所求的8项素数环的各项,打印输出。
(2) 枚举实现8项素数和环
// 8项素数和环枚举求解
#include<stdio.h>
#include<math.h>
void main()
{ int t,k,s,x,g[10],f[10],b[18];long a,y;
for(k=1;k<=15;k++) b[k]=0;
g[9]=1;s=0;
b[3]=b[5]=b[7]=b[11]=b[13]=1; // 5个奇素数标记
printf(" 8项素数和环:
");
for(a=12345678;a<=18765432;a+=9) // 步长为9枚举8位数
{t=0;y=a;
for(k=0;k<=9;k++) f[k]=0;
for(k=1;k<=8;k++)
{x=y%10;f[x]++; // 分离a的8个数字,用f数组统计x的个数
g[9-k]=x; // 用g数组记录a的第k位数字
y=y/10;
}
for(k=1;k<=8;k++)
if(f[k]!=1 || b[g[k]+g[k+1]]!=1) t=1;
if(t==1) continue; // 有相同数字或相邻和非素,返回
s++;
printf(" %d: 1",s); // 输出8项素数和环
for(k=2;k<=8;k++)
printf(",%d",g[k]);
printf("
");
}
}
5-5 递归求解20项素数环
// 递归求解素数环问题
#include<stdio.h>
#include<math.h>
int n,a[2000],b[1000];long s=0;
void main()
{ int t,j,k;
int p(int k);
printf(" 前n个正整数组成素数环,请输入整数n: ");
scanf("%d",&n);
for(k=1;k<=2*n;k++) b[k]=0;
for(k=3;k<=2*n;k+=2)
{for(t=0,j=3;j<=sqrt(k);j+=2)
if(k%j==0)
{t=1;break;}
if(t==0) b[k]=1; // 奇数k为素数的标记
}
a[1]=1;k=2;
p(k);
printf(" 前%d个正整数组成素数环,以上是其中3个。
",n);
}
// 素数环递归函数p(k)
#include <stdio.h>
int p(int k)
{ int i,j,u;
if(k<=n)
{ for(i=2;i<=n;i++)
{ a[k]=i; // 探索第k个数赋值i
for(u=0,j=1;j<=k-1;j++)
if(a[k]==a[j] || b[a[k]+a[k-1]]==0) // 若出现重复数字
u=1; // 若第k数不可置i,则u=1
if(u==0) // 若第k数可置i,则检测是否到n个数
{ if(k==n && b[a[n]+a[1]]==1 && s<3) // 若已到n个数时打印出一个解
{ s++;
printf(" %ld: 1",s);
for (j=2;j<=n;j++)
printf(",%d",a[j]);
printf("
");
}
else
p(k+1); // 若没到m个数,则探索下一个数 p(k+1)
}
}
}
return s;
}
5-6 枚举探索6珠
所能覆盖的最大和s。
// 数码串珠探索
#include<stdio.h>
void main()
{int d,i,j,s,t,u,v,a[20],b[300];
for(s=31;s>=28;s--)
{printf(" s=%2d:
",s); v=0; // v统计s时解的个数
a[0]=0;a[1]=1;a[6]=s;
for(a[2]=a[1]+1;a[2]<=s-4;a[2]++)
for(a[3]=a[2]+1;a[3]<=s-3;a[3]++)
for(a[4]=a[3]+1;a[4]<=s-2;a[4]++)
for(a[5]=a[4]+1;a[5]<=s-1;a[5]++)
{for(i=7;i<=11;i++)
a[i]=s+a[i-6];
for(t=0,i=0;i<=5;i++)
for(j=i+1;j<=i+5;j++)
{t++;b[t]=a[j]-a[i];} // 除s外,产生30个部分和
u=0;
for(d=1;d<=s-1;d++)
for(i=1;i<=30;i++)
if(b[i]==d) // b有[1,s-1]中的一个数,u增1
{u+=1;i=30;}
if(u==s-1) // u=s-1时为完全环覆盖
{v++;
printf(" (%2d) %2d",v,1);
for(i=1;i<=5;i++)
printf(",%2d",a[i+1]-a[i]);
if(v%2==0)
printf("
");
}
}
if(v>0) return;
}
}
5-7 枚举探索4阶德布鲁金环,并与德布鲁金环的回溯程序运行结果进行比较。
求解由16个0或1组成的环序列,形成的由每相连4个数字组成的16个二进制数恰好在环中都出现一次。
(1) 枚举设计要点
约定序列由0000开头,第5个数字与第16个数字显然都为1(否则会出现00000)。余下10个数字应用枚举探求。
设置一维a数组,由约定0000开头,即a(0)~a(3)均为0;a(4)=1,a(15)=1。其余10个数字a(5)~a(14)通过枚举探求。因为是环序列,a(16)~a(18)即为开头的0。
分析10个数字0、1组成的二进制数,高位最多2个0(否则出现0001或0000重复),即循环的初值可定为n1=27。同时,高位不会超过4个1(否则出现11111超界),即循环的终值可定为n2=29+28+27+26。
对区间[n1,n2]中的每一个整数n(为不影响循环,赋值给b),通过除以2取余转化为10个二进制数码。用变量i统计该二进制数中1的个数,若i≠6返回,确保16个数字中有8个1时转入下面的检验:计算m1,m2并通过比较,检验a(0)~a(18)中每4个相连数字组成的二进制数若有相同,显然不能满足题意要求,标记t=1退出。若所有由4个相连数字组成的16个二进制数没有相同的,满足德布鲁金环序列条件,作打印输出。
(2)4阶德布鲁金环序列枚举实现
#include <stdio.h>
void main()
{ int b,m,m1,m2,n,n1,n2,i,j,k,t,a[20];
m=0;
n1=128;
n2=512+256+128+64; // 确定枚举范围
for(n=n1;n<n2;n++)
{for(k=0;k<=18;k++) a[k]=0;
a[4]=1;a[15]=1;
b=n;
for(i=0,k=14;k>=5;k--) // 正整数n(即b)转化为二进制数
{a[k]=b%2; b=b/2;
i+=a[k];
}
if(i!=6) continue; // 确保8个1转入以下检验
for(t=0,k=0;k<=14;k++)
for(j=k+1;j<=15;j++) // 计算并检验16个二进制数是否相同
{m1=a[k]*8+a[k+1]*4+a[k+2]*2+a[k+3];
m2=a[j]*8+a[j+1]*4+a[j+2]*2+a[j+3];
if(m1==m2)
{t=1;break;}
}
if(t==0) // 若16个二进制数没有相同,输出结果
{m=m+1;
printf(" No(%2d): ",m);
for(j=0;j<=15;j++) // 依次输出16个二进制数
printf("%1d",a[j]);
if(m%2==0) printf("
");
}
}
}
5-8 回溯实现组合C(n,m)
对指定的正整数m,n(约定1<m≤n), 回溯实现从n个不同元素中取m个(约定1<m<n)的组合C(n,m)。
(1)回溯算法设计
注意到组合与组成元素的顺序无关,约定组合中的组成元素按递增排序。
设置一维数组a,a(i)(i=1,2,…,m)在1~n中取值。
首先从a(1)=1开始取值。以后各项从前一项增1取值:a[i]=a[i-1]+1。
若a(i)=n+i-m,则返回前一个数组元素a(i-1)。直到i=0,已无法返回,意味着已全部试毕,求解结束。
问题的解空间是由数字1~n组成的m位整数组,其约束条件是没有相同数字。
按以上所描述的回溯的参量:m,n(m≤n)
元素初值:a[1]=1,数组元素初值取1。
取值点:a[i]=a[i-1]+1,以保持升序。
回溯点:a[i]=n+i-m,各数组元素取值至n+i-m后回溯。
(2)回溯实现C(n,m)的C程序实现
// 实现组合C(n,m)
#include <stdio.h>
void main()
{int i,j,n,m,a[100];
long s=0;
printf(" input n (n<10):"); scanf("%d",&n);
printf(" input m(1<m<=n):"); scanf("%d",&m);
i=1;a[i]=1;
while(1)
{if(i==m)
{s++;
for(j=1;j<=m;j++)
printf("%d",a[j]); // 输出一个排列
printf(" ");
if(s%10==0) printf("
");
}
if(i<m)
{i++;a[i]=a[i-1]+1;continue;}
while(a[i]==n+i-m) i--; // 回溯到前一个元素
if(i>0) a[i]++;
else break;
}
printf("
总数为:%ld
",s); // 输出C(n,m)的值
}
5-9 回溯实现复杂排列
应用回溯法探索从n个不同元素中取m(约定1<m≤n)个元素与另外n-m个相同元素组成的复杂排列。
(1)算法设计要点
引入变量k来控制0的个数,当k<n-m时,a[i]=0,元素需从0开始取值;否则,0的个数已达n-m个,a[i]=1,即从1开始取值。这样处理,使0的个数不超过n-m,减少一些无效操作,提高了回溯效率。
按以上所描述的回溯的参量:n,m(m≤n)
元素初值:a[1]=0,数组元素取初值0。
取值点:当k<n-m时,a[i]=0,需从0开始取值;否则,a[i]=1,即从1开始取值。
回溯点:a[i]=n,各元素取值至n时回溯。
约束条件1:a[k]!=0 && a[i]=a[k] || a[i]*a[k]>0 && fabs(a[i]-a[k])=i-k, (其中i>k),排除同一列或同对角线上出现2个皇后。
约束条件2:i=n && h=n-m && b[1-n][1-n]=1, 当取值达n个,其中n-m个零,且棋盘全控时输出一个解。
(2)复杂排列回溯优化设计
#include <stdio.h>
#define N 30
void main()
{int i,j,h,k,n,m,t,a[N];
long s=0;
printf(" input n (n<10):"); scanf("%d",&n);
printf(" input m(1<m<=n):"); scanf("%d",&m);
i=1;a[i]=0; k=1;
while(1)
{t=1;
for(j=1;j<i;j++)
if(a[j] && a[j]==a[i]) {t=0;break;} // 非零元素相同,则返回
if(t && k==n-m && i==n) // 已取n 个值且0的个数为n-m时输出解
{s++;
for(j=1;j<=n;j++) printf("%d",a[j]);
printf(" ");
if(s%10==0) printf("
");
}
if(t && (k<n-m || i<n))
{i++;
if(k<n-m){a[i]=0; k++;} // 0的个数增加1
else a[i]=1; // 若0的个数已达到n-m,则不再取0了
continue;
}
while(a[i]==n) i--; // 调整或回溯或终止
if(i>0)
{if(a[i]==0) k--; // 改变取值为0的元素值前先把0的个数k减1
a[i]++;
}
else break;
}
printf("
s=%ld
",s);
}
5-10 8对夫妇特殊的拍照
一对夫妇邀请了7对夫妇朋友来家餐聚,东道主夫妇编为0号,其他各对按先后分别编为1,2,…,7号。
餐聚后拍照,摄影师要求这8对夫妇男左女右站在一排,东道主夫妇相邻排位在横排的正中央,其他各对排位,1号夫妇中间安排1个人,2号夫妇中间安排2个人,依此类推。
共有多少种拍照排队方式?
- 设计要点
在n组每组2个相同元素(相当于n对情侣),a数组从0取到2n-1不重复,对n同余的两个数为一对编号:余数为0的为0号(即东道主),余数为1的为1号,…,余数为n-1的为n-1号。
例如,n=4,数组元素为0与4,对4同余,为一对“0”; 1与5对4同余,为一对“1”;一般地, i与4+i对4同余,为一对i,(i=0,1,2,3)。
返回条件修改为(当j<i时):
a(j)=a(i) or a(j)%n=a(i)%n and (a(j)>a(i) or a(j)+1!=i-j)
其中a(j)=a(i),为使a数组的2n个元素不重复取值;
a(j)%n=a(i)%n and a(j)>a(i),避免同一对取余相同的数左边大于右边,导致重复;
a(j)%n=a(i)%n and a(j)+1!=i-j,避免同一对数位置相差不满足题意相间要求。
例如,a(j)=0时,此时a(i)=n,为0号情侣,位置应相差1(即中间没有人),即i-j=1。
a(j)=1时,此时a(i)=n+1,为1号情侣,位置应相差2(即中间有1人),即i-j=2。
这些都应满足条件a(j)+1=i-j。如果a(j)+1!=i-j,不满足要求,返回。
设m=2n,若满足条件(g>0 and i=m and a(1)%n<a(m)%n)且a(n)=0(即东道主在正中央),为一个拍照排列,用s统计解的个数。 - 程序实现
// 8对夫妇拍照
#include <stdio.h>
#include <math.h>
void main()
{int i,j,g,n,m,s,a[20];
printf(" input n (2<n): ");
scanf("%d",&n);
m=2*n;
i=1;a[i]=0;s=0;
while(1)
{g=1;
for(j=1;j<i;j++)
if(a[j]==a[i] || a[j]%n==a[i]%n && (a[j]>a[i] || a[j]+1!=i-j))
{g=0;break;} // 出现相同元素或同余小在后时返回
if(g && i==m && a[1]%n<a[m]%n) // 满足统计解的个数条件
{if(a[n]==0) // 满足输出解的条件
{s++;
for(j=1;j<=m;j++)
printf("%d",a[j]%n); // 输出一个排列
printf(" ");
}
}
if(g && i<m)
{i++;a[i]=0;continue;}
while(a[i]==m-1) i--; // 回溯到前一个元素
if(i>0) a[i]++;
else break;
}
printf("
共有解s=%d个。
",s);
}
习题6
6-1 n个矩阵连乘问题
设矩阵A为p行q列,矩阵B为q行r列,求矩阵乘积AB共需做pqr次乘法。
试求n(n>2)个矩阵 的乘积 的最少乘法次数。其中n与 的行、列数 均从键盘输入。
解:注意 是 的列数,也是 的行数,这样才能确保 与 能相乘。
多个矩阵相乘,满足乘运算结合律。
例如,求 ,先求前两个矩阵的乘积 ,还是先求后两个的乘积 ,都是可以的,但两者的乘法次数不一定相等,我们要求最少乘法次数。
设m(i,j)是求乘积 的最少乘法次数,则有递推关系
(i+1=j)
(i≤k≤j,i<j)
初始(边界)条件:m(i,j)=0 (i=j)
最优值为m(1,n).
程序设计:
为递推方便,设置d=i-j。显然,1≤d≤n-1。
// 矩阵连乘
#include <stdio.h>
void main()
{int d,n,i,j,k,t,r[100],m[100][100];
printf(" 请输入矩阵的个数 n :"); scanf("%d",&n);
printf(" 请输入第1个矩阵的行数 :"); scanf("%d",&r[1]);
for(i=1;i<=n-1;i++)
{printf(" 请输入第%d个矩阵的列数,也是第%d个矩阵的行数 :",i,i+1);
scanf("%d",&r[i+1]);
}
printf(" 请输入第%d个矩阵的列数 :",n); scanf("%d",&r[n+1]);
for(i=1;i<=n;i++)
m[i][i]=0;
for(d=1;d<=n-1;d++)
for(i=1;i<=n-d+1;i++)
{j=i+d;
m[i][j]=m[i][i]+m[i+1][j]+r[i]*r[i+1]*r[j+1];
for(k=i+1;k<j;k++)
{t=m[i][k]+m[k+1][j]+r[i]*r[k+1]*r[j+1];
if(t<m[i][j]) m[i][j]=t;
}
}
printf(" %d个矩阵连乘的乘法次数的最小值为:%d
",n,m[1][n]);
}
6-2 应用顺推实现动态规划求解点数值三角形的最优路径
在一个n行的点数值三角形中,寻找从顶点开始每一步可沿左斜(L)或右斜(R)向下至底的一条路径,使该路径所经过的点的数值和最小。
应用顺推实现动态规划求解从项到底的最小路程。
(1)建立递推关系
设点数值三角形的数值存储在二维数组a(n,n),数组b(i,j)为从顶点(1,1)到点(i,j)的最小数值和。b(i,j)与stm(i,j)(i=2,3,…,n)的值由b数组的第i-1行的第j-1个元素与第j个元素值的大小比较决定,即有递推关系:
b(i,j)=a(i,j)+b(i-1,j); (b(i-1,j)<b(i-1,j-1))
b(i,j)=a(i,j)+b(i-1,j-1); (b(i-1,j)≥b(i-1,j-1))
其中i=2,3,…,n
比较b(n,1),b(n,2),…,b(n,n)所得最小值min即为所求的最小路径。
边界条件:
b(1,1)=a(1,1);
b(i,1)=a(i,1)+b(i-1,1);
b(i,i)=a(i,i)+b(i-1,i-1); i=2,…,n。
(2)顺推计算最优值
b[1][1]=a[1][1];
for(i=2;i<=n;i++)
{ b[i][1]=a[i][1]+b[i-1][1];
b[i][i]=a[i][i]+b[i-1][i-1];
}
for(i=3;i<=n;i++) // 顺推得b[n][j]
for(j=2;j<=i-1;j++)
if (b[i-1][j]<b[i-1][j-1])
b[i][j]=a[i][j]+b[i-1][j];
else
b[i][j]=a[i][j]+b[i-1][j-1];
min=10000;
for(j=1;j<=n,j++) // 比较得最短路程
if(b[n][j]<min)
{ min=b[n][j];m=j;}
printf("%d",min);
6-3 应用顺推实现动态规划求解n行m列边数值矩阵最大的路程
已知n行m列的边数值矩阵,每一个点可向右或向下两个去向,试求左上角顶点到右下角顶点的所经边数值和最大的路程。
动态规划算法设计:
设矩阵的行数n,列数m,每点为(i, j),i=1,2,…,n;j=1,2,…,m。显然,该边数值矩阵每行有m−1条横向数值边,每列有n−1条纵向数值边。
从点(i,j)水平向右的边长记为r(i,j)(j<m),点(i,j)向下的边长记为d(i,j)(i<n)。
(1)建立递推关系
设a(i,j)为左上角顶点(1,1)到点(i,j)的最大路程。
a(i,j)的值由a(i-1,j)+d(i,j)与a(i,j-1)+r(i,j)比较,取其较大者得到,即有递推关系:
a(i,j)=max(a(i-1,j)+d(i-1,j),a(i,j-1)+r(i,j-1))
其中i=2,…,n;j=2,…,m。
注意到左边纵列与上边横行只有惟一出口,因而有边界条件:
a(1,1)=0;
a(i,1)=a(i-1,1)+d(i-1,1); i=2,…,n
a(1,j)=a(1,j-1)+r(1,j-1); j=2,…,m
(2)逆推计算最优值
a[1][1]=0;
for(i=2;i<=n;i++)
a[i][1]=a[i-1][1]+d[i-1][1]; // 左边纵列初始化
for(j=2;j<=m;j++)
a[1][j]=a[1][j-1]+r[1][j-1]; // 上边横行初始化
for(i=2;i<=n;i++) // 顺推求解a(i,j)
for(j=2;j<=m;j++)
if(a[i-1][j]+d[i-1][j]>a[i][j-1]+r[i][j-1])
a[i][j]=a[i-1][j]+d[i-1][j];
else
a[i][j]=a[i][j-1]+r[i][j-1];
printf("%d",a[n][m]);
所求左上角顶点到右下角顶点的最大路程即最优值为a(n,m)。
6-4 求解边数值三角形的最短路径
已知边数值三角形每两点间距离如图7-4所示,每一个点可向左或向右两个去向,求三角形顶点到底边的最短路径。
图7-4 三角形边数值数据
- 算法设计
设边数值三角形为n行(不包含作为边终止点的三角形底边),每点为(i,j),i=1,2,……,n;j=1,2,……,i.从点(i,j)向左的边长记为l(i,j),点(i,j)向右的边长记为r(i,j)。记a(i,j)为点(i,j)到底边的最短路程。显然
a(i,j)=min(a(i+1,j)+l(i,j),a(i+1,j+1)+r(i,j))
st(i,j)={‘l’,’r’}
应用逆推求解,所求的顶点A到底边的最短路程为a(1,1). - 边数值三角形最短路径搜索C程序设计
// 边数值三角形最短路径搜索
#include "math.h"
#include <stdio.h>
void main()
{ int n,i,j,t,s;
int a[50][50],l[50][50],r[50][50];char st[50][50];
t=time()%1000;srand(t); // 随机数发生器初始化
printf("请输入数字三角形的行数n:");
scanf("%d",&n);
for(i=1;i<n;i++) j=rand(); // 产生并输出数值边三角形
for(j=1;j<=33;j++) printf(" ");printf(" A
");
for(i=1;i<=n;i++)
{for(j=1;j<=37-4*i;j++) printf(" ");
for(j=1;j<=i;j++) printf(" . "); printf("
");
for(j=1;j<=36-4*i;j++) printf(" ");
for(j=1;j<=i;j++)
{l[i][j]=rand()/1000+1;printf("%4d",l[i][j]);
r[i][j]=rand()/1000+1;printf("%4d",r[i][j]);}
printf("
");}
for(j=1;j<=37-4*(n+1);j++) printf(" ");
for(j=1;j<=n+1;j++) printf(" . ");
printf("底边
");
for(i=n;i>=1;i--) // 逆推求取最短路径
{for(j=1;j<=i;j++)
if(a[i+1][j]+l[i][j]<a[i+1][j+1]+r[i][j])
{a[i][j]=a[i+1][j]+l[i][j];st[i][j]='l';}
else
{a[i][j]=a[i+1][j+1]+r[i][j];st[i][j]='r';}
}
printf("
最短路程为:%d",a[1][1]);
printf("
最短路径为:顶点A ");
for(j=1,i=1;i<=n;i++)
if(st[i][j]=='l')
printf("L-%d-",l[i][j]);
else
{printf("R-%d-",r[i][j]);j++;}
printf("底边。");
}
6-5 求解点数值矩阵最小路径
随机产生一个n行m列的整数矩阵,在整数矩阵中寻找从左上角至右下角,每步可向下(D)或向右(R)或斜向右下(O)的一条数值和最小的路径。
- 算法设计
应用动态规划,即从右下角逐行反推至左上角。确定n,m后,随机产生的整数二维数组a(n,m)作矩阵输出,同时赋给部分和数组b(n,m)。这里数组b(i,j)为点(i,j)到右下角的最小数值和,stm(i,j)是点(i,j)向右(R)或向下(D)或向右下(O)的路标字符数组。
注意到最后一行与最后一列各数只有一个出口,于是由b(n,m)开始向左逐个推出同行的b(n,j),(j=m-1,...,2,1);向上逐个推出同列的b(i,m),(i=n-1,...,2,1)。
b(i,j)与stc(i,j)(i=n-1,...,2,1,j=m-1,...,2,1))的值由同一列其下面的整数b(i+1,j)与同一行其右边的整数b(i,j+1)或其右下方的b(i+1,j+1)的值决定:
首先,作赋值 b(i,j)=yb+b(i+ 1, j + 1): stc(i, j) = "O".(其中变量yb为原b(i,j)的值)。
然后,求 b(i+1,j) 与 b(i,j+1) 的最小值 min。
如果 b(i+1,j+1)>min ,说明前面为b(i,j)赋值不对,作修改:
b(i,j)=yb+min
若min 为 b(i+1,j),则 stc(i,j)="D",否则 stc(i,j)="R"
这样反推所得b(1,1)即为所求的最小路径数字和。
为了打印最小路径,利用c数组从上而下操作:先打印a(1,1),i=1,j=1.
若 stc(i,j)="R" 则j增1,即j=j+1,然后打印 "-R-"与右边整数a(i,j);
若 stc(i,j)="D" 则i增1,即i=i+1,然后打印 "-D-"与下面整数a(i,j);
若 stc(i,j)="O" 则i,j均增1,即i=i+1,j=j+1,然后打印 "-O-"与斜向右下整数a(i,j);
依此类推,直至打印到终点a(n,m)。
6-6 西瓜分堆
已知的n个西瓜的重量分别为整数,请把这堆西瓜分成两堆,每堆的个数不一定相等,使两堆西瓜重量之差为最小。
(1) 设计要点
两组数据之和不一定相等,不妨把较少的一堆称为第1堆。设n个整数b(i)之和为s,则第1堆数据之和s1≤[s/2],这里[x]为x的取整。
问题要求在满足s1≤[s/2]前提下求s1最大值maxc,这样两堆数据和之差的最小值为mind=s-2*maxc。
为了求s1的最大值,应用动态规划设计,按分每一个瓜为一个阶段,共分为n个阶段。每一个阶段都面临两个决策:选与不选该瓜到第1组。
1) 建立递推关系
设m(i,j)为第1堆距离c1=[s/2]还差重量为j,可取瓜编号范围为:i,i+1,…,n的最大装载重量值。则
当0≤j<b(i)时,西瓜i号不可能装入。m(i,j)与m(i+1, j)相同。
而当j≥b(i)时,有两种选择:
不装入西瓜i,这时最大重量值为m(i+1, j);
装入西瓜i,这时已增加重量b(i),剩余重量为j−b(i),可以选择西瓜i+1,…,n来装,最大载重量值为m(i+1,j−b(i))+b(i)。我们期望的最大载重量值是两者中的最大者。于是有递推关系
以上j与b(i)均为正整数,i=1,2,…,n,
所求最优值m(1,c1)即为s1的最大值maxc。因而得两组数据和之差的最小值为mind=s-2maxc=s-2m(1,c1)。
2) 递推计算最优值
for(j=0;j<b(n);j++) m(n,j)=0;
for(j=b(n);j<=c1;j++) m(n,j)=b(n); // 首先计算m(n,j)
for(i=n-1;i>=1;i--) // 逆推计算m(i,j)
for(j=0;j<=c1;j++)
if(j>=b(i) && m(i+1,j)<m(i+1,j-b(i))+b(i))
m(i,j)=m(i+1,j-b(i))+b(i);
else
m(i,j)=m(i+1,j);
printf("%d",m(1,c1));
3) 构造最优解
构造最优解即给出所得最优值时的分瓜方案。
if(m(i,cb)>m(i+1,cb)) (其中cb为当前的剩余量,i=1,2, n−1)
第1堆分b(i);
else 不分b(i);
if(m(1,c1)-sb=b(n)) 则第1堆分b(n)。
(2)求解两组数据和之差的最小值程序设计
// 求解两组数据和之差的最小值
#include <stdio.h>
#define N 40
void main()
{int n,c1,i,j,s,t,cb,sb,b[N],m[N][10*N];
printf(" input n: "); scanf("%d",&n);
s=0;
for(i=1;i<=n;i++) // 输入n个西瓜重量整数
{printf(" 请输入第%d个整数:",i);
scanf("%d",&b[i]); s+=b[i];
}
c1=s/2;
printf(" 各个西瓜重量:");
for(i=1;i<=n;i++)
printf(" %d",b[i]);
printf("
总重量s=%d
",s);
for(j=0;j<b[n];j++)
m[n][j]=0;
for(j=b[n];j<=c1;j++)
m[n][j]=b[n]; // 首先计算m(n,j)
for(i=n-1;i>=1;i--) // 逆推计算m(i,j)
for(j=0;j<=c1;j++)
if(j>=b[i] && m[i+1][j]<m[i+1][j-b[i]]+b[i])
m[i][j]=m[i+1][j-b[i]]+b[i];
else
m[i][j]=m[i+1][j]; // 得最优值m(1,c1)
printf(" 两堆之差最小值为:%d
",s-2*m[1][c1]);
printf(" 第1堆: ");
cb=m[1][c1];
for(sb=0,i=1;i<=n-1;i++) // 构造最优解,输出第1堆的西瓜
if(m[i][cb]>m[i+1][cb])
{cb-=b[i];sb+=b[i];
printf(" %3d",b[i]);
b[i]=0; // b(i)分后赋0,为输出第2堆作准备
}
if(m[1][c1]-sb==b[n])
{printf(" %3d",b[n]);
sb+=b[n]; b[n]=0;
}
printf(" (%d)
",sb);
printf(" 第2堆: ");
for(sb=0,i=1;i<=n;i++) // 输出第2堆西瓜
if(b[i]>0)
{sb+=b[i];
printf(" %3d",b[i]);
}
printf(" (%d)
",sb);
}
6-7 应用递推实现动态规划求解序列的最小子段和
应用递推实现动态规划求解:给定n个整数(可能为负整数)组成的序列 ,求该序列形如 段和的最小值。
递推实现动态规划求解:
1) 动态规划算法设计
设q[j]为序列前j项之和的最小值,即
由q[j]的定义,得q[j]的递推关系:
初始条件:
Q[0]=0 (没有项时,其值自然为0)。
(2) 动态规划程序实现
// 动态规划求最小子段和
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main()
{ int i,j,k,t,n,s,smin,q[1000],a[1000];
t=time(0)%1000;srand(t); // 随机数发生器初始化
printf(" 序列中n个正负项,请确定n:");
scanf("%d",&n);
printf(" 序列的%d个整数为:
",n);
for(i=1;i<=n;i++)
{t=rand()%(4*n)+10; // 随机产生n个整数
if(t%2==1) a[i]=-1*(t-1)/2; // 把奇数变为负数,大小减半
else a[i]=t/2; // 把偶数大小减半
printf("%d,",a[i]);
}
smin=1000;q[0]=0;
for(j=1;j<=n;j++)
{if(q[j-1]>=0) q[j]=a[j];
else q[j]=q[j-1]+a[j];
if(q[j]<smin) //比较得最小值
{smin=q[j];k=j;}
}
printf("
最小子段和为:%ld
",smin);
for(s=0,i=k;i>=1;i--) // 反推最小和子段的首标i
{ s+=a[i]; if(s==smin) break; }
printf(" 最小子段从序列的第%d项到第%d项。
",i,k);
}
6-8 应用递归实现动态规划求解序列的最小子段和
应用递归实现动态规划求解:给定n个整数(可能为负整数)组成的序列 ,求该序列形如 段和的最小值。
递归实现动态规划求解:
1) 动态规划算法设计
设q(j)为序列前j项之和的最小值,即
由q(j)的定义,得q(j)的递推关系:
初始条件:
q(0)=0 (没有项时,其值自然为0)。
(2) 动态规划程序实现
// 动态规划(递归)求最小子段和
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int j,a[1000];
void main()
{ int i,k,n,t,s,smin;
int q(int j);
t=time(0)%1000;srand(t); // 随机数发生器初始化
printf(" 序列中n个正负项,请确定n:"); scanf("%d",&n);
printf(" 序列的%d个整数为:
",n);
for(i=1;i<=n;i++)
{t=rand()%(4*n)+10; // 随机产生n个整数
if(t%2==1) a[i]=-1*(t-1)/2; // 把奇数变为负数,大小减半
else a[i]=t/2; // 把偶数大小减半
printf("%d,",a[i]);
}
smin=1000;
for(j=1;j<=n;j++)
if(q(j)<smin) // 调用递归函数,比较得最小值
{smin=q(j);k=j;}
printf("
最小子段和为:%ld
",smin);
for(s=0,i=k;i>=1;i--) // 反推最小和子段的首标i
{ s+=a[i];
if(s==smin) break; }
printf(" 最小子段从序列的第%d项到第%d项。
",i,k);
}
int q(int j) // 定义递归函数q(j)
{int f;
if(j==0) f=0;
else
{ if(q(j-1)>=0) f=a[j];
else f=q(j-1)+a[j];
}
return f;
}
6-9 插入加号求最小值
在一个n位整数a中插入r个加号,将它分成r+1个整数,找出一种加号的插入方法,使得这r+1个整数的和最小。
1) 动态规划求解
设f(i,k)表示在前i位数中插入k个加号所得和的最小值,a(i, j)表示从第i个数字到第j个数字所组成的j−i+1(i≤j)位整数值。
为了求取f(i,k),考察数字串的前i个数字,设前j(k≤j<i)个数字中已插入k−1个加号的基础上,在第j个数字后插入第k个乘号,显然此时的最小和为f(j,k−1)+a(j+1,i)。于是可以得递推关系式:
f(i,k)=min(f(j,k−1)+a(j+1,i)) (k≤j<i)
前j个数字没有插入乘号时的值显然为前j个数字组成的整数,因而得边界值为:
f(j,0)=a(1,j) (1≤j≤i)
为简单计,在程序设计中省略a数组,用变量d替代。
2) 程序设计
// 在一个数字串中插入r个+号,使和最小
#include <stdio.h>
#include <string.h>
void main()
{ char sr[16];
int n,i,j,k,u,r,b[16],t[16],c[16][16];
double f[17][17],d;
printf("请输入整数:"); scanf("%s",sr);
n=strlen(sr);
printf("请输入插入的+号个数r:");
scanf("%d",&r);
if(n<=r)
{printf(" 输入的整数位数不够或r太大! ");
return;}
printf("在整数%s中插入%d个+号,使和最小:
",sr,r);
for(d=0,j=0;j<=n-1;j++)
b[j]=sr[j]-48; // 把输入的数串逐位转换到b数组
for(i=1;i<=n;i++)
for(j=1;j<=r;j++)
f[i][j]=1e16;
for(d=0,j=1;j<=n;j++)
{d=d*10+b[j-1]; // 把b数组的一个字符转化为数值
f[j][0]=d; // f[j][0]赋初始值
}
for(k=1;k<=r;k++)
for(i=k+1;i<=n;i++)
for(j=k;j<i;j++)
{for(d=0,u=j+1;u<=i;u++)
d=d*10+b[u-1];
if(f[i][k]>f[j][k-1]+d) // 递推求取f[i][k]
{f[i][k]=f[j][k-1]+d;
c[i][k]=j;
}
}
t[r]=c[n][r];
for(k=r-1;k>=1;k--)
t[k]=c[t[k+1]][k]; // 逆推出第k个+号的位置t[k]
t[0]=0;t[r+1]=n;
for(k=1;k<=r+1;k++)
{for(u=t[k-1]+1;u<=t[k];u++)
printf("%c",sr[u-1]); // 输出最优解
if(k<r+1)
printf("+");
}
printf("=%.0f
",f[n][r]); // 输出最优值
}
6-10 根据例6-1求解整币兑零不同的兑换种数的递推算法与例6-2 求解整币兑零的最少零币个数的动态规划算法,写出完整程序。
- 求解整币兑零不同的兑换种数程序设计
// 整币兑零递推求解
#include<stdio.h>
void main()
{ int p,i,j,m,n,k;static int t[12];
long b,s; static long a[12][1001];
printf("请输入整币值n(单位数):"); // 输入处理数据
scanf("%d",&n);
printf("请输入零币种数m:");
scanf("%d",&m);
printf("(从小至大依次输入每种零币值)
");
for(i=1;i<=m;i++)
{ printf("第%d种零币值(单位数):",i);
scanf("%d",&t[i]);
}
for(j=0;j<=n;j++) // 确定初始条件
if(j%t[1]==0) a[1][j]=1;
else a[1][j]=0;
for(s=a[1][n],i=2;i<=m;i++) // 递推计算a(2,n),a(3,n),...
{ for(j=t[i];j<=n;j++)
{ p=j-t[i];b=0;
for(k=1;k<=i;k++) b+=a[k][p];
a[i][j]=b;
}
s+=a[i][n]; // 累加a(1,n),a(2,n),...
}
printf("整币兑零种数为:%ld
",s); // 输出兑零种数
}
- 求解整币兑零最少零币个数程序设计
// 整币兑零,最少零币个数动态规划求解
#include<stdio.h>
void main()
{ int i,j,m,n;
static int t[12],g[20][1001];
printf(" 请输入整币值(单位数):"); // 输入处理数据
scanf("%d",&n);
printf(" 请输入零币种数:");
scanf("%d",&m);
printf(" (从小至大依次输入每种零币值)
");
for(i=1;i<=m;i++)
{ printf(" 第%d种零币值(单位数):",i);
scanf("%d",&t[i]);
}
for(j=1;j<=n;j++)
if(j%t[1]!=0) g[1][j]=0;
else g[1][j]=j/t[1];
for(i=2;i<=m;i++)
for(j=1;j<=n;j++)
{ if(j<t[i] || j>t[i] && g[i][j-t[i]]==0)
g[i][j]=g[i-1][j];
else
g[i][j]=g[i][j-t[i]]+1;
}
printf(" 最少零币个数为:%d
",g[m][n]); // 输出最少零币个数
}
习题7
7-1 删除数字求最小值
给定一个高精度正整数a, 去掉其中s个数字后按原左右次序将组成一个新的正整数。对给定的a,s寻找一种方案,使得剩下的数字组成的新数最小。
解:应用贪心算法设计求解
(1) 设计要点
操作对象为n位高精度数,存储在数组a中。
在整数的位数固定的前提下,让高位的数字尽量小,整数的值就小。这就是所要选取的贪心策略。
每次删除一个数字,选择一个使剩下的数最小的数字作为删除对象。
当k=1时,在n位整数中删除哪一个数字能达到最大的目的?从左到右每相邻的两个数字比较:若出现减,即左边大于右边,则删除左边的大数字。若不出现减,即所有数字全部降序或相等,则删除左边的大数字。
当k>1(当然小于n),按上述操作一个一个删除。每删除一个数字后,后面的数字向前移位。删除一个达到最小后,再从头即从串首开始,删除第2个,依此分解为k次完成。
若删除不到k个后已无左边大于右边的降序或相等,则停止删除操作,打印剩下串的左边n−k个数字即可(相当于删除了若干个最右边的数字)。
3. 贪心算法程序设计
// 贪心删数字达最小
#include<stdio.h>
void main()
{ int i,j,k,m,n,x,a[200];
char b[200];
printf(" 请输入整数:");
scanf("%s",b); // 以字符串方式输入高精度整数
for(n=0,i=0;b[i]!=' ';i++)
{n++;a[i]=b[i]-48;}
printf(" 删除数字个数: ");scanf("%d",&k);
printf(" 以上%d位整数中删除%d个数字分别为: ",n,k);
i=0;m=0;x=0;
while(k>x && m==0)
{i=i+1;
if(a[i-1]>a[i]) // 出现递减, 删除左边的数字
{ printf("%d, ",a[i-1]);
for(j=i-1;j<=n-x-2;j++)// 删除一数字后,后面的数字前移
a[j]=a[j+1];
x=x+1; // x统计删除数字的个数
i=0; // 从头开始查递增区间
}
if(i==n-x-1) m=1; // 已无递减区间,m=1脱离循环
}
printf(" 删除后所得最小数: ");
for(i=1;i<=n-k;i++) // 打印剩下的左边n−k个数字
printf("%d",a[i-1]);
printf("
");
}
7-2 枚举求解埃及分数式
本章应用贪心算法构造了埃及分数式:3/11=1/5+1/15+1/165,试用枚举法求解分数3/11的所有3项埃及分数式,约定各项分母不超过200。
解:(1) 设计要点
设指定的分数m/d的三个埃及分数的分母为a,b,c (a<b<c),最大分母不超过z,通过三重循环实施枚举。
确定a循环的起始值a1与终止值a2为:
(即把b,c全放大为z)
(即把b,c全缩减为a)
b循环起始取a+1,终止取z-1.
c循环起始取b+1,终止取z.
对于三重循环的每一组a,b,c,计算x=mabc,y=d(ab+bc+ca).
如果x=y 且 b,c不等于d,即满足分解为三个埃及分数的条件,打印输出一个分解式。然后退出内循环,继续寻求。
(2)构建指定分数的3个埃及分数式
// 构建三个埃及分数之和
#include <stdio.h>
void main()
{int a1,a2,a,b,c,d,m,n,z; double x,y;
printf(" 确定分数m/d,请输入m,d: ");
scanf("%d,%d",&m,&d);
printf(" 请确定分母的上界:");
scanf("%d",&z);
printf( " 把分数%d/%d分解为三个埃及分数之和:
",m,d);
printf( " (分母不得为%d,最大分母不超过%d)
",d,z);
n=0;
a1=d*z/(m*z-2*d); a2=d*3/m+1;
for(a=a1;a<=a2;a++)
for(b=a+1;b<=z-1;b++)
for(c=b+1;c<=z;c++)
{x=m*a*b*c; // 计算x,y值
y=d*(a*b+b*c+c*a);
if(x==y && b!=d && c!=d) // 输出分解式
{ n=n+1;
printf(" NO%d: %d/%d=1/%d",n,m,d,a);
printf("+1/%d+1/%d
",b,c);
break;
}
}
printf(" 共上述%d个分解式.
",n);
}
7-3 币种统计
单位给每个职工发工资(约定精确到元),为了保证不至临时兑换零钱,且使每个职工取款的张数最少,请在取工资前统计所有职工所需的各种票面(约定为100,50,20,10,5,2,1元共7种)的张数,并验证币种统计是否正确。
(1) 算法设计
各职工的工资额依次从键盘输入,同时用su统计工资总额。
为了确保各职工所得款的张数最少,应用“贪心”策略,优先取大面值币种,即首先付100元币;小于100元时,优先付50元币;依此类推。
设置b数组,存储7种票面的值,即b[1]=100,b[2]=50,…,b[7]=1。
设置s数组,存储对应票面的张数,即s[1]为100元的张数,…,s[7]为1元的张数。
最后验证:各种票面的总额su1是否等于su? 若相等,验证正确。
(2) 程序实现
// 币种统计
#include<stdio.h>
void main()
{ int i,j,m,n,gz; long su1,su=0;
int s[8]={0,0,0,0,0,0,0,0};
int b[8]={0,100,50,20,10,5,2,1};
printf(" 请输入人数:");
scanf("%d",&n);
printf(" 请依次输入各职工的工资:
");
for(i=1;i<=n;i++)
{ printf(" 输入第%d个职工工资:",i);
scanf("%d",&gz);
su=su+gz;
for(j=1;j<=7;j++)
{ m=gz/b[j];
s[j]=s[j]+m;
gz=gz-m*b[j];
}
}
printf(" 单位工资总额为: %ld
",su);
printf(" 各面值币的统计结果:
");
su1=0;
for(j=1;j<=7;j++)
{ printf(" %3d---%3d
",b[j],s[j]);
su1=su1+b[j]*s[j];
}
if(su==su1) printf(" 经检验统计无误!
");
}
7-4 只显示两端的取数游戏
A与B玩取数游戏:随机产生的2n个整数排成一排,但只显示排在两端的数。两人轮流从显示的两端数中取一个数,取走一个数后即显示该端数,以便另一人再取,直到取完。
胜负评判:所取数之和大者为胜。
A的取数策略:“取两端数中的较大数”这一贪心策略。
B的取数策略:当两端数相差较大时,取大数;当两端数相差为1时,随意选取。
试模拟A与B取数游戏进程,2n个整数随机产生。
(1) 算法要点
设置k循环(k=1——2n),当k%2=1时A取数,k%2=0时B取数,体现了A先取,A,B轮留取数。
每次显示排两端整数为d[k]与d[2n],通过比较其中较大者t为所取数,并分别加入A的得分sa。B的取数从键盘输入,所取数t加入B的得分sb。
特别地,当A、B所取数t=d[2n],则前面的数均需后移一位:
d[j]=d[j-1]; (j=2n,2n-1,…,k)
这样处理,为后续取数提供方便。
取数完毕,比较最后得分即可评定胜负。
算法操作为取数与移位,时间复杂度为O(n2)。
(2) 程序实现
// 模拟A,B取数游戏
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main()
{ int j,k,n,sa,sb,t,c[1000],d[1000];
sa=sb=0;
t=time(0)%1000;srand(t); // 随机数发生器初始化
printf(" 序列中2n个整数, 请确定n:");
scanf("%d",&n);
for(j=1;j<=2*n;j++)
{c[j]=rand()%(2*n)+2; // 随机产生2n个整数
d[j]=c[j];
}
printf(" 序列的%d个整数已产生,每次只显示两端整数。
",2*n);
printf(" A先取,A,B轮流取,直到取完。
");
for(k=1;k<=2*n;k++)
{if(k<2*n) printf("
两端数为:%2d,%2d ",d[k],d[2*n]);
else printf("
只剩下1个数:%2d ",d[2*n]);
if(k%2==1)
{t=d[k];
if(t<d[2*n])
{t=d[2*n];
for(j=2*n;j>=k+1;j--) d[j]=d[j-1];
}
sa=sa+t; printf(" A取数%2d; ",t);
}
else
{ printf(" B取数:");scanf("%d",&t);
if(t==d[k] || t==d[2*n])
{ sb=sb+t;
if(t==d[2*n])
{ for(j=2*n;j>=k+1;j--) d[j]=d[j-1];}
}
else
{ printf(" A取数有误,重新开始!"); return;}
}
}
printf(" 原序列的%d个整数为:",2*n);
for(j=1;j<=2*n;j++)
printf(" %d",c[j]);
printf("
最后得分为 A=%d, B=%d,",sa,sb);
if(sa>sb) printf(" 此游戏A胜!
");
else if(sa<sb) printf(" 此游戏B胜!
");
else printf(" 此游戏A,B平手!
");
}
7-5 全显取数游戏 “先取不败”的实现
A与B玩取数游戏:随机产生的2n个整数排成一排,但只显示排在两端的数。两人轮流从显示的两端数中取一个数,取走一个数后即显示该端数,以便另一人再取,直到取完。
胜负评判:所取数之和大者为胜。
A说:还是采用贪心策略,每次选取两端数中较大者为好。虽不能确保胜利,但胜的几率大得多。
B说:我可以确保不败,但有两个条件:一是我先取;二是明码,即所有整数全部显示。
试模拟A、B的取数游戏。
(1) 算法要点
应用贪心策略每次取两端较大数不能确保B先取不败。
为确保B先取不败,建立数学模型:
设序列的2n个整数存储于a[1]——a[2n],
1) 计算序列中奇数号整数之和s1与偶数号整数之和s2。
2) 如果s1>s2,B取所有奇数号整数:先取a[1],则A必取偶数号(2或2n)上的整数;随后B“连号”取数,即A若取a[2],B取a[3]; A若取a[2n],B取a[2n-1];…这样可确保B取完所有奇数号整数而获胜。
3) 否则,即s1≤s2,B取所有偶数号整数:先取a[2n],则A必取奇数号(1或2n-1)上的整数;随后B“连号”取数,即A若取a[1],B取a[2]; A若取a[2n-1],B取a[2n-2];…这样可确保B取完所有偶数号整数而不败(当s1=s2时平手)。
4) A按贪心策略取数,即取两端数的较大者。
5) 算法操作为取数与移位,时间复杂度为O(n2)。
(2) 程序实现
// 所有数显示,B先取不败取数游戏
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main()
{ int j,k,n,d1,d2,s1,s2,t,a[1000];
s1=s2=0;
t=time(0)%1000;srand(t); // 随机数发生器初始化
printf(" 序列中2n个整数, 请确定n:");
scanf("%d",&n);
printf(" 序列的%d个整数依次为:",2*n);
for(j=1;j<=2*n;j++)
{a[j]=rand()%(2*n)+2; // 随机产生并显示2n个整数
printf(" %d",a[j]);
if(j%2==1) s1+=a[j];
else s2+=a[j];
}
printf("
B先取。");
d1=1;d2=2*n;
if(s1>s2)
{printf(" B取数%d;
",a[d1]);
d1=d1+1;
}
else
{printf(" B取数%d;
",a[d2]);
d2=d2-1;
}
printf(" 请在剩余项:");
for(j=d1;j<=d2;j++) printf(" %d",a[j]);
printf(" 的两端取数。
");
for(k=2;k<=n;k++)
{ if(a[d1]>a[d2])
{ printf(" A取数: %d;",a[d1]);
printf(" B取数: %d;
",a[d1+1]);
d1=d1+2;
}
else
{ printf(" A取数: %d;",a[d2]);
printf(" B取数: %d;
",a[d2-1]);
d2=d2-2;
}
printf(" 请在剩余项:");
for(j=d1;j<=d2;j++) printf(" %d",a[j]);
printf(" 的两端数取数。
");
}
printf(" A最后取数: %d;
",a[d2]);
if(s1>s2)
{ printf(" 最后得分为:B=%d, A=%d
",s1,s2);
printf(" 此游戏B胜!
");
}
else if(s1<s2)
{ printf(" 最后得分为:B=%d, A=%d
",s2,s1);
printf(" 此游戏B胜!
");
}
else
{ printf(" 最后得分为:B=%d, A=%d
",s2,s1);
printf(" 此游戏B与A平手!
");
}
}
习题8
8-1 连写数探求
从1开始按正整数的顺序不间断连续写下去所成的整数称为连写数。要使连写数123456789101112…m(连写到整数m)能被指定的整数p(<1000)整除,m至少为多大?
(1)模拟除法设计要点
要使连写数1234...m能被键盘指定的整数n整除,模拟整数的除法操作:
设被除数为a,除数为n,商为b,余数为c,则
b=a/n, c=a-bn 或 c=a%n
当c≠0且m为1位数时,a=c10+m 作为下一轮的被除数继续。
当c≠0一般地m为一个t位数时,则分解为t次(即循环t次)按上述操作完成。
直至c=0时,连写数能被n整除,作打印输出增连数1234...m除以n所得的商。
在整个模拟除法过程中,m按顺序增1。
(2) 模拟除法程序设计
// 模拟除法求连写数
#include <math.h>
#include<stdio.h>
void main()
{ int c,d,e,j,k,t,w,m,n; long a;
printf(" A给出整数n: ");
scanf("%d",&n);
c=0;m=0;
while(1)
{m++;j=m;
{e=j/10;t=1;w=1;
while(e>0) // 对每一个j,计算t
{e=e/10;t=t*10;w=w+1;}
e=j;
for(k=1;k<=w;k++) // 对每一个j,分位试商
{d=e/t;e=e%t;t=t/10;
a=c*10+d;c=a%n;}
}
if(c==0)
{printf(" B寻求的整数m:%d.
",m);
printf(" 连写数12...%d/%d=",m,n);
c=1;
for(j=2;j<=m;j++)
{e=j/10;t=1;w=1;
while(e>0) // 对每一个j,计算t
{e=e/10;t=t*10;w=w+1;}
e=j;
for(k=1;k<=w;k++) // 对每一个j,分位试商
{d=e/t;e=e%t;t=t/10;
a=c*10+d;c=a%n;
printf("%d",a/n);}
}
printf("
");
}
if(c==0) break;
}
}
8-2 01串积
程序设计爱好者A,B进行计算游戏:
B任给一个正整数b,A寻求另一个整数a, 使a 与b的积最小且全为0与1组成的数。
例如,B给出b=23,A找到a=4787, 其最小01串积为110101。
- 设计要点
01串积问题相对前面的积全为“1”的乘数问题要复杂一些,我们应用求余数判别。
(1) 注意到01串积为十进制数,应用求余运算“%”可分别求得个位“1”,十位“1”,…,分别除以已给b的余数,存放在c数组中:c(1)为1,c(2)为10除以b的余数,c(3)为100除以b的余数,…。
(2) 要从小到大搜索01串,不重复也不遗漏,从中找出最小的能被b整除01串积。为此,设置k从1开始递增,把k转化为二进制,就得到所需要的这些串。不过,这时每个串不再看作二进制数,而要看作十进制数。
(3) 在某一k转化为二进制数过程中,每转化一位a(i)(0或1),求出该位除以b的余数a(i)c(i),通过累加求和得k转化的整个二进制数除以b的余数s。
(4) 判别余数s 是否被b整除:若s%b=0, 即找到所求最小的01串积。
(5) a 从高位开始除以b的商存储在d数组,实施整数除法运算:
x=e10+a[j]; // e为上轮余数,x为被除数
d[j]=x/b; // d为a 从高位开始除以b的商
e=x%b; // e为试商余数
去掉d数组的高位“0”后,输出d即为所寻求的数。
(6) 最后从高位开始打印a数组,即为01串积。
2. 程序设计
// 01串积C程序
#include<stdio.h>
void main()
{ int b,e,i,j,t,x,a[2000],d[2000],c[2000];
long k,s;
printf(" B给出整数 b:"); scanf("%d",&b);
c[1]=1;
for(i=2;i<200;i++)
c[i]=10*c[i-1]%b; // c(i)为右边第i位1除以b的余数
k=1;
while(1)
{ k++;j=k;i=0;s=0;
while(j>0)
{i++;a[i]=j%2;
s+=a[i]*c[i];j=j/2; s=s%b; // 除2取余法转化为二进制
}
if(s%b==0)
{for(e=0,j=i;j>=1;j--)
{ x=e*10+a[j];
d[j]=x/b; e=x%b; // a 从高位开始除以b的商为d
}
j=i;
while(d[j]==0) j--; // 去掉d数组的高位“0”
printf(" A寻求整数a:");
for(t=j;t>=1;t--)
printf("%d",d[t]);
printf("
a*b的最小01串积为:");
for(t=i;t>=1;t--)
printf("%d",a[t]);
printf("
");
break;
}
}
}
8-3 自然对数底e的高精度计算
自然对数的底数e是一个无限不循环小数, 是“自然律”的一种量的表达,在科学技术中用得非常多。学习了高数后我们知道,以e为底数的对数是最简的,用它是最“自然”的,所以叫“自然对数”。
试设计程序计算自然对数的底e,精确到小数点后指定的x位。
1.算法设计
(1)选择计算公式
计算自然对数的底e,我们选用以下公式:
(1)
(2)确定计算项数
其次,要依据输入的计算位数x确定所要加的项数n。显然,若n太小,不能保证计算所需的精度;若n太大,会导致作过多的无效计算。
可证明,式中分式第n项之后的所有余项之和 。因此,只要选取n,满足 即可。即只要使
(2)
于是可设置对数累加实现计算到x位所需的项数n。为确保准确,算法可设置计算位数超过x位(例如x+2位),只打印输出x位。
(3)竖式除模拟
设置a数组,下标预设5000,必要时可增加。计算的整数值存放在a(0),小数点后第i位存放在a(i)中(i=1,2,…)。
依据公式(1),应用竖式除模拟进行计算:
数组除以n,加上1;再除以n−1,加上1;…。这些数组操作设置在j (j=n,n-1,…,2) 循环中实施。
按公式实施除竖式计算操作:被除数为c,除数d分别取n,n−1,……,2。商仍存放在各数组元素(a(i)=c/d)。余数(c%d)乘10加在后一数组元素a(i+1)上,作为后一位的被除数。
按数组元素从高位到低位顺序输出。因计算位数较多,为方便查对,每一行控制打印50位,每10位空一格。注意,在输出结果时,整数部分a(0)需加1。
(4) 模拟乘除竖式计算求解e,程序运行非常快捷。注意到其计算项数n小于计算e的位数x,该算法的时间复杂度为O(xn)。
2.自然对数的底e的程序实现
// 高精度计算自然对数的底e
#include <math.h>
#include<stdio.h>
void main()
{ double s; int x,n,c,i,j,d,l,a[5000];
printf(" 请输入精确位数:");
scanf("%d",&x);
for(s=0,n=2;n<=5000;n++) // 累加确定计算的项数n
{ s=s+log10(n);
if (s>x) break;
}
for(i=0;i<=x+2;i++)
a[i]=0;
for(c=1,j=n;j>=2;j--) // 按公式分步计算
{d=j;
for(i=0;i<=x+1;i++) // 各位实施除j
{a[i]=c/d;
c=(c%d)*10+a[i+1];
}
a[x+2]=c/d;
a[0]=a[0]+1;c=a[0]; // 整数位加1
}
printf("
e=%d.",a[0]+1); // 遂位输出计算结果
for(l=10,i=1;i<=x;i++)
{ printf("%d",a[i]);
l++;
if (l%10==0) printf(" ");
if (l%50==0) printf("
");
}
printf("
");
}
8-4 进站时间模拟
根据统计资料,车站进站口进一个人的时间至少为2秒,至多为8秒。试求n个人进站所需时间。
(1)随机模拟算法
一个人的进站时间至少为2秒,至多为8秒,设时间精确到小数点后一位,则每一个人进站的时间在2.0,2.1,2.2,…,8.0等数据中随机选取。
应用C语言库函数srand(t)进行随机数发生器初始化,其中t为所取的时间秒数。这样可避免随机数从相同的整数取值。C库函数中的随机函数rand()产生−90~32767之间的随机整数,在随机模拟设计时,为产生区间[a,b]中的随机整数,可以应用C语言的整数求余运算实现:
rand()%(b−a+1)+a;
为简化设计,把每一个人的进站时间乘以10转化为整数,即每一个人的进站时间为rand()%61+20,随机取值范围为20,21,22,…,80,单位为1/10秒。则n个人的进站时间为
for(t=0,i=1;i<=n;i++)
t=t+rand()%61+20;
求和完成后,转化为时间的分,秒输出。
(2)进站时间模拟程序实现
// 进站时间模拟
#include <stdio.h>
void main()
{int i,n,m,s; long t;
printf("请输入进站人数n:");
scanf("%d",&n);
t=time()%1000;srand(t); // 随机数发生器初始化
printf("%d人进站所需时间约为:",n);
for(t=0,i=1;i<=n;i++)
t=t+rand()%61+20; // 计算进站时间总和
m=t/600;
s=(t%600)/10; // 转化为分秒输出
printf("%d分%d秒.
",m,s);
}
8-5 模拟扑克升级发牌
模拟扑克升级发牌,把含有大小王的共54张牌随机分发给4家,每家12张,底牌保留6张。
1.模拟算法设计
(1)模拟花色与点数
模拟发牌必须注意随机性。所发的一张牌是草花还是红心,是随机的;是5点还是J点,也是随机的。
同时要注意不可重复性。如果在一局的发牌中出现两个黑桃K就是笑话了。同时局与局之间必须作到互不相同,如果某两局牌雷同,也不符合发牌要求。
为此,对应4种花色,设置随机整数x,对应取值为1~4。对应每种花色的13点,设置随机整数y,对应取值为1~13。为避免重复,把x与y组合为三位数:z=x100+y,并存放在数组m(54)中。发第i+1张牌,产生一个x与y,得一个三位数z,数z与已有的i个数组元素m(0),m(1),…m(i−1)逐一进行比较,若不相同则打印与x,y对应的牌(相当于发一张牌)后,然后赋值给m(i),作为以后发牌的比较之用。若有相同的,则重新产生随机整数x与y得z,与m数组值进行比较。
(2)模拟大小王
注意到在升级扑克中有大小王,它的出现给程序设计带来一定的难度。大小王的出现也是随机的,为此,把随机整数y的取值放宽到0~13,则z可能有100,200,300,400。定义z=200时对应大王,z=100时对应小王,同上作打印与赋值处理。若z=300或400,则返回重新产生x与y。
(3)随机生成模拟描述
在已产生i张牌并存储在m数组中,产生第i+1张牌的模拟算法:
for(j=1;j<=10000;j++)
{x=rand()%4+1; y=rand()%14; // x表花色,y表点数
z=x100+y;
if(z300 || z400) continue;
t=0;
for(k=0;k<=i−1;k++)
if(zm[k]) {t=1;break;} // 与前产生的牌比较确保牌不重复
if(t0)
{m[i]=z;break;} // 产生的新牌赋值给m(i)
}
(4)打印输出
打印直接应用C语言中ASCII码1~6的字符显示大小王与各花色。设置字符数组d,打印点数时把y=1、13、12、11分别转化为A、K、Q、J。
为实现真正的随机,根据时间的不同,设置t=time()%10000;srand(t) 初始化随机数发生器,从而达到真正随机的目的。
2.发扑克牌C程序实现
// 发扑克升级牌,有大小王,4个人每人12张牌,底牌6张.
#include <stdio.h>
void main()
{int x,y,z,t,i,j,k,m[55];
char d[14]=" A234567891JQK";
printf("
E S W N
");
t=time()%1000;srand(t); // 随机数发生器初始化
m[0]=0;
for(i=1;i<=54;i++)
{if(i==49) printf("bottom:
");
for(j=1;j<=10000;j++)
{x=rand()%4+1; y=rand()%14;
z=x*100+y;
if(z==300 || z==400) continue;
t=0;
for(k=0;k<=i−1;k++)
if(z==m[k]) {t=1;break;} // 确保牌不重复
if(t==0)
{m[i]=z;break;}
}
if(z==100 || z==200) printf(" %c ",x);
else if(y==10) printf(" %c10 ",x+2);
else printf(" %c%c ",x+2,d[y]);
if(i%4==0) printf("
");
}
printf("
");
}
8-6 特殊洗牌模拟
给你2n张牌,编号为1,2,3,…n,n+1,…,2n,这也是最初牌的顺序。一次洗牌是把序列变为n+1,1,n+2,2,n+3,3,n+4,4,…,2n,n。可以证明,对于任意自然数n,都可以在经过m次洗牌后重新得到初始的顺序。
编程对于小于10000的自然数n(n从键盘输入)的洗牌,求出重新得到初始顺序的洗牌次数m的值,并显示洗牌过程。
1.过程模拟设计
设洗牌前位置k的编号为p(k),洗牌后位置k的编号变为b(k)。
我们寻求与确定洗牌前后牌的顺序改变规律。
前n个位置的编号赋值变化:位置1的编号赋给位置2,位置2的编号赋给位置4,……,位置n的编号赋给位置2n。即b(2k)=p(k)(k=1,2,…,n)。
后n个位置的编号赋值变化:位置n+1的编号赋给位置1,位置n+2的编号赋给位置3,……,位置2n的编号赋给位置2n−1。即b(2k−1)=p(n+k)(k=1,2,…,n)。
约定洗牌10000次(可增减),设置m循环,在m循环中实施洗牌,每次洗牌后检测是否得到初始的顺序。
2.模拟洗牌过程程序实现
#include<stdio.h>
void main()
{int k,n,m,y,p[10000],b[10000];
printf("
n=");scanf("%d",&n);
printf("
");
for(k=1;k<=2*n;k++) // 最初牌的顺序
{p[k]=k; printf("%d ",p[k]);}
for(m=1;m<=20000;m++)
{y=0;
for(k=1;k<=n;k++) // 实施一次洗牌
{b[2*k]=p[k];
b[2*k−1]=p[n+k];}
for(k=1;k<=2*n;k++)
p[k]=b[k];
printf("
%d: ",m); // 打印第m次洗牌后的结果
for(k=1;k<=2*n;k++)
printf("%d ",p[k]);
for(k=1;k<=2*n;k++) // 检测是否回到初始的顺序
if(p[k]!=k) y=1;
if(y==0)
{printf("
m=%d
",m);break;} // 输出回到初始的洗牌次数
}
}
习题9
9-1 完成递归求解最大子段和程序
以上递归求解最大子段和没有标明最大子段位置,应如何标明位置?请完善递归程序求解最大子段和。
// 递归求最大子段和
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int i1,i2,j1,sum,a[1000];
void main()
{ int i,n,s,t;
int ms(int m1,int m2);
t=time(0)%1000;srand(t); // 随机数发生器初始化
printf(" 序列中n个正负项,请确定n:");
scanf("%d",&n);
printf(" 序列的%d个整数为:
",n);
for(i=1;i<=n;i++)
{t=rand()%(4*n)+10; // 随机产生n个整数
if(t%2==1) a[i]=-1*(t-1)/2; // 把奇数变为负数,大小减半
else a[i]=t/2; // 把偶数大小减半
printf("%d,",a[i]);
}
s=ms(1,n);
printf("
最大子段和为:%d
",s);
if(a[i2]==s)
printf(" 最大子段为序列的第%d项。
",i2);
else
printf(" 最大子段从序列的第%d项到第%d项。
",i1,j1);
}
int ms(int m1,int m2) // 定义递归函数ms()
{int i,m,sm1,sm2,s1,s2,ts;
sum=0;
if(m1==m2) // 递归出口
{if(a[m1]>0) sum=a[m1];
else sum=0;
}
else
{ m=(m1+m2)/2; // 序列分解
sm1=ms(m1,m); // 对应情形①,递归求解
sm2=ms(m+1,m2); // 对应情形②,递归求解
s1=0;
for(ts=0,i=m;i>=m1;i--)
{ ts+=a[i];
if(ts>s1)
{s1=ts;i1=i;} // s1为求到第m项的最大值
}
s2=0;
for(ts=0,i=m+1;i<=m2;i++)
{ ts+=a[i];
if(ts>s2)
{s2=ts;j1=i;} // s2为求到第m+1项的最大值
}
sum=s1+s2;
if(sum<sm1)
{sum=sm1;i2=m1;} // 比较合并
if(sum<sm2)
{sum=sm2;i2=m2;}
}
return sum;
}
9-2 递归实现设置障碍的马步遍历
在一个n行m列棋盘中,任指定一处障碍。请设计递归程序,寻求一条起点为(1,1)越过障碍的遍历路径。
// 递归探求n×m棋盘设置障碍的马步遍历
#include <stdio.h>
int k,n,m,x1,y1,z,d[20][20]={0};
void main()
{ int g,q,x,y;
int tr(int g,int x,int y);
printf(" 棋盘为n行m列,请输入n,m: ");
scanf("%d,%d",&n,&m);
printf(" 指定障碍位置(x1,y1),请输入x1,y1: ");
scanf("%d,%d",&x1,&y1);
g=2;z=0;x=1;y=1; // 起点约定为(1,1)
d[x][y]=1;
q=tr(g,x,y); // 调用tr(g,x,y)
if(z>0)
printf(" 共有以上%d个指定马步路径.
",z);
else printf(" 未找到指定路径!
");
}
// 马步路径递归函数
int tr(int g,int x,int y)
{int i,j,u,v,k=0,q=0;
int a[9]={0,2,1,-1,-2,-2,-1,1,2}; // 按可能8位给a,b赋初值
int b[9]={0,1,2,2,1,-1,-2,-2,-1};
while(q==0 && k<8)
{ k=k+1;u=x+a[k];v=y+b[k]; // 探索第k个可能位置
if(u>0 && u<=n && v>0 && v<=m && d[u][v]==0 && !(u==x1 && v==y1))
{ d[u][v]=g; // 所选位走第g步
if(g==m*n-1)
{z++;
printf(" 第%d个指定障碍的马步路径为:
",z);
for(i=1;i<=n;i++) // 以二维形式输出一个解
{for(j=1;j<=m;j++)
if(i==x1 && j==y1)
printf(" ×");
else printf("%4d",d[i][j]);
printf("
");
}
g=g-1;
}
else q=tr(g+1,u,v);
if(q==0) d[u][v]=0; // 实施回溯
if(g==2 && k==8)
q=1; // 回溯完,则返回
}
}
return q;
}
9-3 回溯设计探求一个n行m列马步哈密顿圈
// 马步哈密顿圈回溯程序设计
#include <stdio.h>
void main()
{ int i,j,k,q,u,v;
int n,m,d[20][20]={0},x[400]={0},y[400]={0},t[400]={0};
int a[9]={0,2,1,-1,-2,-2,-1,1,2}; // 按可能8位给a,b赋初值
int b[9]={0,1,2,2,1,-1,-2,-2,-1};
printf(" 棋盘为n行m列,请输入n,m: ");
scanf("%d,%d",&n,&m);
i=1; u=1;v=1;
x[i]=u;y[i]=v;d[u][v]=1; // 起始位置赋初值
while(i>0)
{q=0; // 尚未找到第i+1步方向
for(k=t[i]+1;k<=8;k++)
{ u=x[i]+a[k];v=y[i]+b[k]; // 探索第k个可能位置
if(u>0 && u<=n && v>0 && v<=m && d[u][v]==0) // 所选位为空可走
{ x[i+1]=u;y[i+1]=v;d[u][v]=i+1; // 则走第i+1步
t[i]=k; // 记录第i+1步方向
q=1;break;
}
}
if(q==1 && i==m*n-1)
{ if(u==2 && v==3 || u==3 && v==2)
{ printf(" 此哈密顿圈的一个解为:
");
for(j=1;j<=n;j++) // 以二维形式输出遍历解
{for(k=1;k<=m;k++)
printf("%4d",d[j][k]);
printf("
");
}
return;
}
t[i]=d[x[i]][y[i]]=d[x[i+1]][y[i+1]]=0; i--; // 实施回溯,寻求新的解
}
else if(q==1) i++; // 继续探索
else
{t[i]=d[x[i]][y[i]]=0; i--; } // 实施回溯
}
}
9-4 纵向双拼哈密顿圈
设计用起点为(1,1),终点为(2,2)或(1,3)的遍历,实现纵向双拼哈密顿圈.
- 纵向双拼设计要点
设一个起点为(1,1)的n行m列马步遍历路径的终点为(2,2)或(1,3),则可拼接成纵向双拼为一个2n行m列的组合哈密顿圈。
同时要实现左上角置“1”的习惯,注意到A的每一项在B基础上增加m*n,左上角实为元素d(n,1),因而可设c=d(n,1)-1,组合圈的每一项均减去c,这样左上角置“1”,非正项每项加2mn。 - 程序实现
// 纵向双拼组合哈密顿圈
#include <stdio.h>
int k,m,n,z,d[20][20]={0};
void main()
{ int c,i,j,g,q,x,y;
int t(int g,int x,int y);
printf(" 组合元素为n行m列,请确定n,m: ");
scanf("%d,%d",&n,&m);
g=2;z=0;x=1;y=1;
d[x][y]=1; // 起始位置赋初值
q=t(g,x,y); // 调用t(g,x,y)
if(z>0)
{printf(" 一个%d行%d列组合型哈密顿圈:
",2*n,m);
c=d[n][1]-1;
for(i=n;i>=1;i--)
{for(j=1;j<=m;j++) // 输出倒行遍历
if(d[i][j]-c>0)
printf("%4d",d[i][j]-c);
else
printf("%4d",d[i][j]-c+2*m*n);
printf("
");
}
for(i=1;i<=n;i++)
{for(j=1;j<=m;j++) // 输出原遍历
printf("%4d",d[i][j]+m*n-c);
printf("
");
}
}
else printf(" 未找到指定路径!
");
}
// 指定马步路径递归函数
int t(int g,int x,int y)
{int u,v,k=0,q=0;
int a[9]={0,2,1,-1,-2,-2,-1,1,2}; // 按可能8位给a,b赋初值
int b[9]={0,1,2,2,1,-1,-2,-2,-1};
while(q==0 && k<8)
{ k=k+1;u=x+a[k];v=y+b[k]; // 探索第k个可能位置
if(u>0 && u<=n && v>0 && v<=m && d[u][v]==0) // 所选位为空可走
{ d[u][v]=g; // 则走第g步
if(g==m*n)
{if(u==2 && v==2 || u==1 && v==3) // 原遍历终点为(2,2)或(1,3)
{z++;q=1;return q; }
g=g-1;
}
else q=t(g+1,u,v);
if(q==0) d[u][v]=0; // 实施回溯
if(g==2 && k==8)
q=1; // 回溯完,则返回
}
}
return q;
}