10.4 竞赛题目选讲
例题10-22 统计问题(The Counting Problem, ACM/ICPC Shanghai 2004, UVa1640)
给出整数a、b,统计a和b(包含a和b)之间的整数中,数字0,1,2,3,4,5,6,7,8,9分别出现了多少次。1≤a,b≤108。注意,a有可能大于b。
【分析】
解决这类题目的第一步一般都是:令fd(n)表示0~n-1中数字d出现的次数,则所求的就是fd(b+1)-fd(a)。例如,要统计0~234中4的个数,可以分成几个区间,如表10-2所示。
表10-2 0~234所划区间
| 范围 | 模板集 |
| 0~9 | * |
| 10~99 | ** |
| 100~199 | 1** |
| 200~229 | 20*, 21*, 22* |
| 230~234 | 230, 231, 232, 233, 234 |
表10-2中的“模板”指的是一些整数的集合,其中字符“*”表示“任意字符”。例如,1**表示以1开头的任意3位数。因为后两个数字完全任意,所以“个位和十位”中每个数字出现的次数是均等的。换句话说,在模板1**所对应的100个整数的200个“个位和十位”数字中,0~9各有20个。而这些数的百位总是1,因此得到:模板1**对应的100个整数包含数字0,2~9各20个,数字1有120个。
这样,只需把0~n分成若干个区间,算出每个区间中各个模板所对应的整数包含每个数字各多少次,就能解决原问题了,细节留给读者思考。
例题10-23 多少块土地(How Many Pieces of Land?, UVa10213)
有一块椭圆形的地。在边界上选n(0≤n<231)个点并两两连接得到n(n-1)/2条线段。它们最多能把地分成多少个部分?如图10-12所示,n=6时最多能分成31份。
图10-12 n=6时所划分的土地
【分析】
本题需要用到欧拉公式:在平面图中,V-E+F=2,其中V是顶点数,E是边数,F是面数。因此,只需要计算V和E即可(注意还要减去外面的“无限面”)。
不管是顶点还是边,计算时都要枚举一条从固定点出发(所以最后要乘以n)的对角线,它的左边有i个点,右边有n-2-i个点。左右点的连线在这条对角线上形成i(n-2-i)个交点,得到i(n-2-i)+1条线段。每个交点被重复计算了4次,每条线段被重复计算了2次。
本题还有一个有趣之处:n=1~n=6时答案分别为1、2、4、8、16、31。如果根据前5项“找规律”得到“公式”2n-1,即就错了。
例题10-24 ASCII面积(ASCII Area, NEERC 2011, UVa1641)
在一个h*w(2≤h,w≤100)的字符矩阵里用“.”、“\”和“/”画出一个多边形,计算面积。如图10-13所示,面积为8。
图10-13 ASCII面积
【分析】
这是一道和几何相关的题目,不过不需要高深的几何知识。每个格子要么全白,要么全黑,要么半白半黑,只要能准确地判断出来即可。字符“\”和“/”都是半白半黑,问题在于“.”到底是全白还是全黑。
解决方法是从上到下从左到右处理,沿途统计“/”和“\”。当这两个字符出现偶数次时说明接下来的格子在多边形外;奇数次则说明接下来的格子在多边形内。
例题10-25 约瑟夫的数论问题(Joseph's Problem, NEERC 2005, UVa1363)
输入正整数n和k(1≤n,k≤109),计算
。
【分析】
被除数固定,除数逐次加1,直观上余数也应该有规律。假设k/i的整数部分等于p,则k mod i = k-i*p。因为k/(i+1)和k/i差别不大,如果k/(i+1)的整数部分也等于p,则k mod (i+1) = k-(i+1)*p = k-i*p - p = k mod i - p。换句话说,如果对于某一个区间i, i+1, i+2,…, j,k除以它们的商的整数部分都相同,则k除以它们的余数会是一个等差数列。
这样,可以在枚举i时把它所在的等差数列之和累加到答案中。这需要计算满足[k/j]=[k/i]=p的最大j。
例题10-26 帮帮Tomisu(Help Mr. Tomisu, UVa11440)
给定正整数N和M,统计2和N!之间有多少个整数x满足:x的所有素因子都大于M(2≤N≤107,1≤M≤N,N-M≤105)。输出答案除以100000007的余数。例如,N=100,M=10时答案为43274465。
【分析】
因为M≤N,所以N!是M!的整数倍。“所有素因子都大于M”等价于和M!互素。另外,根据最大公约数的性质,对于k>M!,k与M!互素当且仅当k mod M!与M!互素。这样,只需要求出“不超过M!且与M!互素的正整数个数”,再乘以N!/M!即可。这样,问题的关键就是求出phi(M!)。因为有多组数据,考虑用递推的方法求出所有的phifac(n)=phi(n!)。由phi函数的公式:
如果n不是素数,那么n!和(n-1)!的素因子集合完全相同,因此phifac(n)=phifac(n-1)*n;如果n是素数,那么还会多一项(1-1/n),即(n-1)/n,约分得phifac(n)=phifac(n-1)*(n-1)。
核心代码如下(请读者注意其中的细节,如m=1的情况):
int main() {
int n, m;
sieve(10000000); //筛法求素数
phifac[1] = phifac[2] = 1; //请读者思考,为什么phifac[1]等于1而不是0
for(int i = 3; i <= 10000000; i++) //递推phifac[i]=phi(i!)%MOD
phifac[i] = (long long)phifac[i-1] * (vis[i] ? i : i-1) % MOD; //vis[i]为真
i不是素数
while(scanf("%d%d", &n, &m) == 2 && n) {
int ans = phifac[m];
for(int i = m+1; i <= n; i++) ans = (long long)ans * i % MOD;
printf("%d\n", (ans-1+MOD)%MOD); //注意这里要减1,因为题目从2开始统计
}
return 0;
}
例题10-27 树林里的树(Trees in a Wood, UVa10214)
在满足|x|≤a,|y|≤b(a≤2000,b≤2000000)的网格中,除了原点之外的整点(即x,y坐标均为整数的点)各种着一棵树。树的半径可以忽略不计,但是可以相互遮挡。求从原点能看到多少棵树。设这个值为K,要求输出K/N,其中N为网格中树的总数。如图10-14所示,只有黑色的树可见。
【分析】
显然4个坐标轴上各只能看见一棵树,所以可以只数第一象限(即x>0,y>0),答案乘以4后加4。第一象限的所有x, y都是正整数,能看到(x,y),当且仅当gcd(x,y)=1。
由于a范围比较小,b范围比较大,一列一列统计比较快。第x列能看到的树的个数等于0<y≤b的数中满足gcd(x,y)=1的y的个数。可以分区间计算。
换句话说,每次需要计算phi(x)和进行O(x)次直接判断,计算phi(x)需要O(x1/2)时间,而直接判断只需要O(1)时间。再加上枚举x的所有a种可能,总时间为O(a2)。
例题10-28 (问题抽象)高速公路(Highway, ACM/ICPC CERC 2006, UVa1393)
有一个n行m列(1≤n,m≤300)的点阵,问:一共有多少条非水平非竖直的直线至少穿过其中两个点?如图10-15所示,n=2,m=4时答案为12,n=m=3时答案为14。
| ![]() |
| 图10-14 树林里的树 | 图10-15n行m 列点阵 |
【分析】
不难发现两个方向是对称的,所以只统计“\”型的,然后乘以2。方法是枚举直线的包围盒大小a*b,然后计算出包围盒可以放的位置。首先,当gcd(a,b)>1时肯定重复了,如图10-16(a)所示,大包围盒a*b满足gcd(a,b)>1,在它的对角线和a'*b'的对角线是同一条直线(其中a'=a/gcd(a,b),b'=b/gcd(a,b))。
其次,如果放置位置不够靠左,也不够靠上,则它和它“左上方”的包围盒也重复了,如图10-16(b)所示。
| ![]() |
| (a) | (b) |
图10-16 gcd(a,b)>1时示意图
假定左上角坐标为(0,0),则对于左上角在(x,y)的包围盒,其“左上方”的包围盒的左上角为(x-a,y-b)。这个“左上角”合法的条件是x-a≥0且y-b≥0。
包围盒本身不出界的条件是x+a≤m-1, y+b≤n-1,一共有(m-a)(n-b)个,而“左上方”有包围盒的情况,即a≤x≤m-a-1且b≤y≤n-b-1,有c = max(0, m-2a) * max(0, n-2b)种放法。相减得到:a*b的包围盒有(m-a)(n-b)-c种放法。
另外要注意应预处理保存所有gcd,而不是边枚举边算,否则会超时。
例题10-29 魔法GCD(Magical GCD, ACM/ICPC CERC 2013, UVa1642)
输入一个n(n≤100000)个元素的正整数序列
,求一个连续子序列,使得该序列中所有元素的最大公约数与序列长度的乘积最大。例如,5个元素的序列30, 60, 20, 20, 20的最优解为{60, 20, 20, 20},乘积为gcd(60,20,20,20)*4=80。
【分析】
本题看上去和第8章介绍的一些“传统算法题”很像,所以可试着沿用这样一个常见的框架:从左到右枚举序列的右边界j,然后快速求出左边界i≤j,使得MGCD(i,j)最大,其中MGCD(i,j)定义为
。
如何快速求出i呢?好像那些“传统方法”(单调队列等)都用不上,因为gcd函数并没有很多“好用”的代数性质。怎么办?还是从数论的角度入手吧。考虑序列5, 8, 6, 2, 6, 8,当j=5时需要比较i=1, 2, 3, 4, 5时的MGCD(i,j),如表10-3所示。
表10-3 j=5时比较i的MGCD(i,j)
| i | gcd表达式 | gcd值 | 序列长度 |
| 1 | gcd(5,8,6,2,6) | 1 | 5 |
| 2 | gcd(8,6,2,6) | 2 | 4 |
| 3 | gcd(6,2,6) | 2 | 3 |
| 4 | gcd(2,6) | 2 | 2 |
| 5 | gcd(6) | 6 | 1 |
从下往上看,gcd表达式里每次多一个元素,有时gcd不变,有时会变小,而且每次变小时一定是变成了它的某个约数(想一想,为什么)。换句话说,不同的gcd值最多只有log2j种!当gcd值相同时,序列长度越大越好,所以可以把表10-3简化成表10-4中的形式。
表10-4 简化表10-3
| gcd值 | 1 | 2 | 6 |
| i | 1 | 2 | 5 |
因为表里只有log2j个元素,所以可以依次比较每一个i对应的MGCD(i,j),时间复杂度为O(logj)。下面考虑j从5变成6时,这个表会发生怎样的变化。首先,上述所有gcd值都要再和a6=8取gcd,即表10-4中第一行的1, 2, 6分别变成gcd(1,8)=1,gcd(2,8)=2,gcd(6,8)=2。然后要加入i=6的序列,gcd值为8。由于相同的gcd值只需要保留i的最小值,所以i=5被删除,最终得到如表10-5所示结果。
表10-5 i=5被删除后的结果
| gcd值 | 1 | 2 | 6 |
| i | 1 | 2 | 8 |
上述过程需要删除gcd相同的重复元素,但因为元素个数只有O(logj)个,即使用二重循环比较,时间效率也是很高的,每次修改表10-5的时间复杂度为O((logj)2),总时间复杂度为O(n(logn)2)。但因为很难构造出每次表里都有接近log2j个元素的数据,实际运行时间和时间复杂度为O(nlogn)的算法相当。