PS:题解来源于USACO官方,实际上是想练练翻译和表达能力,请不要在意细节,看不懂就继续看USACO的英文版题解吧……
题目:【传送门 http://www.usaco.org/index.php?page=viewproblem2&cpid=435】
官方题解:【传送门 http://www.usaco.org/current/data/sol_odometer_silver.html】
题目大意:给出一个范围 X..Y(X<=Y<=10^18),求出该范围中“有趣数字”的个数。对“有趣数字”的定义是:一个数字去掉前导0后至少一半数位上的数字相同。例如3223和1100是,而97791和123不是。
都明白 X..Y 中有趣的数字个数实际上等于 1..Y 中有趣数字的个数减去 1..X-1 中的数字个数吧?我们只要考虑如何求 1..a 中的个数 f(a) 即可。
要求 f(a) ,我们可以通过自己构造数字的方法统计个数。因为要求数字中有一半以上的数位上的数字相同,我们只需枚举这个重复的数字是 0..9 的哪一个就是了,设为targ。
定义 F[i][und][k][is0],i 表示已经枚举了前 i 位数字,und 作为布尔值判断当前枚举的这个数字是否能保证小于a(Pascal的话und可以用0或1来表示),k 表示目前数字targ出现的次数与其他数字出现的次数之差,is0 作为布尔值表示当前是否在枚举数字的前导0。F中计算的就是满足上述条件的数的个数。
自然,可以枚举添加第i+1位数字,设为next。如果und = false且next > a[i+1](a中第i+1位数),枚举得到的这个i+1位的数(下文称为now)就不合法了,因为在无法保证当前数字小于a(也就是说 1..i 个数位都和a对应的数位上数字相同)的情况下,不能添加一个比a对应数位上的数字还大的数(否则就可以肯定now >= a)。如果und = true 或者next < a[i+1] 那么now就可以保证小于a,定义x = true,否则x = false。如果next = 0且is0 = true 就表示现在仍然在枚举前导0,定义y = true否则y = false。最后如果next = targ,就说明targ出现的次数与其他数字出现的次数差增大了1,nk = k +1 否则 nk = k -1。最后F[i+1][x][nk][y] += F[i][und][k][is0]。初值F[0][false][0][true] = 1,答案很容易想到是Sum(F[n][x][k][false]),n表示a的位数,x既可以是true也可以是false,k >= 0 保证targ出现次数大于n的一半。
实际上,有一种情况我们计数重复,就是当位数为偶数时数字刚好由两个数字组成(如3322,144141等),这些数计了2次。怎么计算这些数字的个数呢?因为位数最大只有18,我们完全可以考虑通过枚举targ1和targ2的方法枚举这些数字,应该不会超时。或者可以重复使用相同的动态规划方程F[i][und][k][is0],只不过next的取值只有2个,且答案只能是Sum(F[n][x][0][false])。减掉就是 f(a) 的数值了。
Pascal代码如下:
const
maxn=40;
st=20;
var
f:array[0..maxn,0..1,0..maxn+1,0..1] of int64;
a,b:string;
x,y:int64;
function dp(v:string;l,x,y:longint):int64;
var
i,j,k,z,nx,nj,nk,nz:longint;
now:int64;
begin
fillchar(f,sizeof(f),0);
f[0,0,st,1]:=1;
for i:=0 to l-1 do
for j:=0 to 1 do
for k:=0 to maxn do
for z:=0 to 1 do begin
now:=f[i,j,k,z];
if now=0 then continue;
for nx:=0 to 9 do begin
if (y>-1) and ((nx>0) or (z=0)) and (nx<>x) and (nx<>y) then continue;
if (j=0) and (nx>ord(v[i+1])-48) then continue;
if (z=1) and (nx=0) then nz:=1 else nz:=0;
if (nx