10.4 竞赛题目选讲

例题10-22 统计问题(The Counting Problem, ACM/ICPC Shanghai 2004, UVa1640)

给出整数ab,统计ab(包含ab)之间的整数中,数字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是面数。因此,只需要计算VE即可(注意还要减去外面的“无限面”)。

不管是顶点还是边,计算时都要枚举一条从固定点出发(所以最后要乘以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≤hw≤100)的字符矩阵里用“.”、“\”和“/”画出一个多边形,计算面积。如图10-13所示,面积为8。

图10-13 ASCII面积

【分析】

这是一道和几何相关的题目,不过不需要高深的几何知识。每个格子要么全白,要么全黑,要么半白半黑,只要能准确地判断出来即可。字符“\”和“/”都是半白半黑,问题在于“.”到底是全白还是全黑。

解决方法是从上到下从左到右处理,沿途统计“/”和“\”。当这两个字符出现偶数次时说明接下来的格子在多边形外;奇数次则说明接下来的格子在多边形内。

例题10-25 约瑟夫的数论问题(Joseph's Problem, NEERC 2005, UVa1363)

输入正整数nk(1≤nk≤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,…, jk除以它们的商的整数部分都相同,则k除以它们的余数会是一个等差数列。

这样,可以在枚举i时把它所在的等差数列之和累加到答案中。这需要计算满足[k/j]=[k/i]=p的最大j

 

例题10-26 帮帮Tomisu(Help Mr. Tomisu, UVa11440)

给定正整数NM,统计2和N!之间有多少个整数x满足:x的所有素因子都大于M(2≤N≤107,1≤MNN-M≤105)。输出答案除以100000007的余数。例如,N=100,M=10时答案为43274465。

【分析】

因为MN,所以N!是M!的整数倍。“所有素因子都大于M”等价于和M!互素。另外,根据最大公约数的性质,对于k>M!,kM!互素当且仅当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|≤ba≤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)

有一个nm列(1≤n,m≤300)的点阵,问:一共有多少条非水平非竖直的直线至少穿过其中两个点?如图10-15所示,n=2,m=4时答案为12,n=m=3时答案为14。

 

  图10-14 树林里的树     图10-15nm  列点阵

【分析】

不难发现两个方向是对称的,所以只统计“\”型的,然后乘以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+am-1, y+bn-1,一共有(m-a)(n-b)个,而“左上方”有包围盒的情况,即axm-a-1且byn-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)

输入一个nn≤100000)个元素的正整数序列,求一个连续子序列,使得该序列中所有元素的最大公约数与序列长度的乘积最大。例如,5个元素的序列30, 60, 20, 20, 20的最优解为{60, 20, 20, 20},乘积为gcd(60,20,20,20)*4=80。

【分析】

本题看上去和第8章介绍的一些“传统算法题”很像,所以可试着沿用这样一个常见的框架:从左到右枚举序列的右边界j,然后快速求出左边界ij,使得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)的算法相当。