1.1 机理分析法的实验范例
根据客观事物的特性,分析其内部的机理,弄清关系,在适当抽象的条件下,得到可以描述事物属性的数学工具。
经过数学分析,如果能够抽象出Ad Hoc类问题的内在规律,则可以采用机理分析法建立数学模型,然后根据模型的原理对应到算法,编程实现,通过执行算法得到问题解,如图1.1-1所示。
机理分析法的核心是数学建模,即使用适当的数学思想建立起模型;或者提取问题中的有效信息,用简明的方式表达其规律。需要注意的是:
1)选择的模型必须尽量多地体现问题的本质特征。但这并不意味着模型越复杂越好,累赘的信息会影响算法效率。
2)模型的建立不是一个一蹴而就的过程,而是要经过反复地检验和修改,在实践中不断完善。
3)数学模型通常有严格的格式,但程序编写形式可不拘一格。
机理分析法是一个复杂的数据抽象过程。我们要善于透视问题的本质,寻找突破口,进而选择适当的模型。模型的构造过程可以帮助我们认识问题,不同的模型从不同的角度反映问题,可以引发不同的思路,起到引导发散思维的作用。但认识问题的最终目的是解决问题,模型的固有性质虽可帮我们建立算法,其优劣也可通过时空复杂度等指标来分析和衡量,但最终还是以程序的运行结果为标准。所以模型不是一成不变的,同样要通过各种技术不断优化。模型的产生虽然是人脑思维的产物,但它仍然是客观事物在人脑中的反映。所以要培养良好的建模能力,还必须依靠在平时的学习中积累丰富的知识和经验。
下面举两个实验范例。
【1.1.1 Factstone Benchmark】
【问题描述】
Amtel已经宣布,到2010年,它将发行128位计算机芯片;到2020年,它将发行256位计算机;等等,Amtel坚持每持续十年将其字大小翻一番的战略。(Amtel于2000年发行了64位计算机,在1990年发行了32位计算机,在1980年发行了16位计算机,在1970年发行了8位计算机,并首先在1960年发行了4位计算机。)
Amtel将使用新的标准检查等级(Factstone)来宣传其新芯片大大提高的能力。Factstone等级被定义为这样的最大整数n,使得n!可以表示为一个计算机字中的无符号整数(比如1960年的4位计算机可表示3!=6,而不能表示4!=24)。
给出一个年份1960≤y≤2160,Amtel最近发布的芯片的Factstone等级是什么?
输入:
给出若干测试用例。每个测试用例一行,给出年份y。在最后一个测试用例后,给出包含0的一行。
输出:
对于每个测试用例,输出一行,给出Factstone等级。
![image image](https://i-blog.csdnimg.cn/blog_migrate/5af226f1e36da6413fddedc948f81210.png)
试题来源:Waterloo local 2005.09.24
在线测试地址:POJ 2661,UVA 10916
试题解析
对于给定的年份,先求出当年Amtel处理器的字大小,然后计算出最大的n的值,使得n!成为一个符合字的大小的无符号整数。
在1960年,字的大小是4位,以后每十年字的大小翻一番,这就意味着,y年字的位数为k=22+y-196010。能放在k位中最大的无符号整数是2k-1。如果n!为不大于2k-1的最大正整数,则n为y年芯片的Factstone等级。计算方法有两种:
方法1:直接求不大于2k-1的最大正整数n!,这种方法极容易溢出且速度慢。
方法2:采用对数计算,即根据log2n!=log2n+log2(n-1)+…+log21≤log2(2k-1)计算y年字的位数k,累加log2i(i从1出发,每次加1),直到数字超过k为止。此时,i-1即为Factstone等级。
程序清单
#include <stdio.h>
#include <math.h>
int y,Y,i,j,m; // 年份为y
double f,w;// y年字的位数为w,log2i的累加值为f
main(){
while (1 == scanf("%d",&y) && y){// 输入年份y
w = log(4);// 按照每十年字的大小翻一番的规律,计算y年字的位数w
for (Y=1960; Y<=y; Y+=10){
w *= 2;
}
i = 1;// 累加log2i(每次i加1),直到数字超过w
f = 0;
while (f < w) {
f += log((double)++i);
}
printf("%d\n",i-1) ;// 输出Factstone等级
}
if (y) printf("fishy ending %d\n",y);
}
【1.1.2 Bridge】
【问题描述】
n个人要在晚上过桥,在任何时候最多两人一组过桥,每组要有一只手电筒。在这n个人中只有一只手电筒可以用,因此要安排以某种往返的方式来返还手电筒,使得更多的人可以过桥。
每个人的过桥速度不同,每组的速度由速度较慢的成员所决定。请确定一个策略,让n个人用最少的时间过桥。
输入:
输入的第一行给出n,接下来的n行给出每个人的过桥时间,不会超过1000人,且没有人的过桥时间超过100秒。
输出:
输出的第一行给出所有n个人过桥的总的秒数,接下来的若干行给出实现策略。每行包含一个或两个整数,表示组成一组过桥的一个人或两个人(每个人用其在输入中给出的过桥所用的时间来标识。虽然许多人的过桥时间相同,但即使有混淆,对结果也没有影响)。这里要注意的是过桥也有方向性,因为要返还手电筒让更多的人通过。如果用时最少的策略有多个,则任意一个都可以。
试题来源:Waterloo local 2000.09.30
在线测试地址:POJ 2573,ZOJ 1877,UVA 10037
试题解析
稍动脑筋,便可以得出一个简单的逻辑:要使得n个人用最少的时间过桥,慢的成员必须借助快的成员传递手电筒。
由于一次过桥最多两人且手电筒需要往返传递,因此以两个成员过桥为一个分析单位,计算过桥时间。我们按过桥时间递增的顺序将n个成员排序。设当前序列中,
A是最快的人,B是次快的人,A和B是序列首部的两个元素。
a是最慢的人,b是次慢的人,a和b是序列尾部的两个元素。
有两种过桥方案:
方案1:用最快的成员A传递手电筒帮助最慢的a和b过桥。
如果带一个最慢的成员,则所用的时间是a+A(a表示最快和最慢的两个成员到对岸所需的时间,而A是最快的成员返回所需的时间)。显然带两个最慢的成员所用的时间是2*A+a+b。
方案2:用最快的成员A和次快的成员B传递手电筒帮助最慢的a和b过桥。
步骤1:A和B到对岸,所用时间为B。
步骤2:A返回,将手电筒给最慢的a和b,所用时间为A。
步骤3:a和b到对岸,所用时间为a;到对岸后,他们将手电筒交给B。
步骤4:B需要返回原来的岸边,因为要交还手电筒,所需时间为B。
所以,需要的总时间为2*B+A+a。
显然,最慢的a和b要用最少的时间过桥,只能借助A或者A和B传递手电筒过桥,其他方法都会增加过桥时间。哪一种过桥方案更有效?比较一下就行了:
如果(2A+a+b<2B+A+a),则采用第1种方案,即用最快的成员A传递手电筒;否则采用第2种方案,即用最快的成员A和次快的成员B传递手电筒(2A+a+b<2B+A+a等价于b+A<2*B)。
我们每次帮助最慢的两个成员过桥(n-=2),累计每个最佳过桥方案的时间。最后产生两种可能情况:
1)对岸剩下2个队员(n==2),全部过桥,即累计时间为B。
2)对岸剩下3个队员(n==3),用最快的成员传递手电筒,帮助最慢的成员过桥,然后与次慢的成员一起过桥,即累计时间为a+A+b。
程序清单
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<string>
using namespace std;
int n,i,j,k,a[111111]; // 人数为n,每个人的速度存储于序列a[]
int ans=0;// n个人过桥的总时间初始化
int main () {
scanf("%d",&n);// 输入每个人的速度
for(i=1;i<=n;i++)scanf("%d",a+i);
if(n==1){// 输出1个人的过桥方案
printf("%d\n%d\n",a[1],a[1]);return 0;
}
int nn=n;
sort(a+1,a+n+1);// 按照速度递增顺序排序
while(n>3){// 统计n个人过桥的总时间
if(a[1]+a[n-1]<2*a[2]){// 累计用a[1]传递手电筒帮助最慢的2个成员过桥
// 所需的时间
ans+=a[n]+a[1]*2+a[n-1];
}else{// 累计用a[1]a[2]传递手电筒帮助最慢的2个成员
// 过桥所需的时间
ans+=a[2]+a[1]+a[2]+a[n];
}
n-=2;// 两个最慢的成员过桥
}
if(n==2)ans+=a[2];// 对岸剩下2个成员,累计其过桥的时间
else ans+=a[1]+a[2]+a[3];// 对岸剩下3个成员,累计其过桥的时间
printf("%d\n",ans);// 输出n个人过桥的总时间
n=nn;
while(n>3){// 输出每组人过桥所用的时间
if(a[1]+a[n-1]<2*a[2])// 输出用a[1]传递手电筒的过桥方案
printf("%d %d\n%d\n%d %d\n%d\n",a[1],a[n],a[1],a[1],a[n-1],a[1]);
else// 输出用a[1]和a[2]传递手电筒的过桥方案
printf("%d %d\n%d\n%d %d\n%d\n",a[1],a[2],a[1],a[n-1],a[n],a[2]);
n-=2;// 两个最慢的成员过桥
}
if(n==2)printf("%d %d\n",a[1],a[2]);// 剩下2个队员过桥,输出过桥方案
else// 剩下3个队员过桥,输出过桥方案
printf("%d %d\n%d\n%d %d\n",a[1],a[3],a[1],a[1],a[2]);
return 0;
}