时光荏苒,两年前作为参赛者参加了第15届NIT校赛,一年前作为监考人员参加了第16届NIT校赛,今年有幸成为第17届NIT校赛出题组的一员,贡献了一题非(wu)常(ren)友(sheng)好(huan)的数位DP。
出题过程可以用迷惑来形容。题是自己出的,代码也是自己写的,然而,对拍程序发现数位DP代码和暴力代码跑出的答案不一致(后来发现应该使用四进制状态压缩而不是二进制状态压缩 )。
做法 :
首先考虑如何确定单个数字是否是“Triangle Numbers”,可以直接暴力枚举所有情况找合法三角形。
当然也可以使用贪心策略,先将数位分解之后排序,每次找连续的三个数字。看是否能组成三角。
接下来是最关键的,如何将搜索记忆化。即考虑这样的情况,当前已经枚举一些高位(或者说已经确定数字的前缀),剩下一些低位(后缀)还没有枚举,且这些低位的枚举没有限制,每一位都可以是0-9的任意一个。一个简单直观的想法是以0-9这十种数字的出现次数和可枚举的后缀长度作为前缀等价类的划分依据。被划分至同一个等价类的前缀关于“有多少个Triangle Numbers”的问题具有一致的答案,一个等价类只需要暴力计算一次结果并将结果记录到记忆化数组中,之后再遇到直接根据记忆化数组返回结果即可。基于这个想法和本题的数据范围,需要开辟大约
10
∗
1
0
10
10*10^{10}
10∗1010的空间用于存储每个状态(等价类)的结果,显然开不下这样的空间。考虑放宽等价类的划分依据,实际上某个数字X出现3次以上都可以当作它出现了3次。因为X在某个合法三角形中最多出现3次。这样每个数字只有0,1,2,3四种情况,容易想到四进制状态压缩来保存每个数字的出现次数。这样空间就变成了
10
∗
4
10
10*4^{10}
10∗410=10485760。
#include <bits/stdc++.h>
using namespace std;
int num[11];
int tmpNum[11];
int dp[11][1 << 20];
int cnt[10];
bool check_Triangle (int a, int b, int c)//三边是否能组成三角形
{
int v[] = {a, b, c};
sort(v, v + 3);
return v[0] + v[1] > v[2];
}
bool checkNum(int x)//该数字是否是三角形数
{
int bit = 0;
while (x)
{
tmpNum[++bit] = x % 10;
x /= 10;
}
for (int i = 1; i <= bit; i++)
for (int j = i + 1; j <= bit; j++)
for (int k = j + 1; k <= bit; k++)
if (check_Triangle (tmpNum[i], tmpNum[j], tmpNum[k]))
return 1;
return 0;
}
int dfs(int pos, bool limit, int sta, int now)
{
//sta压缩记录了0-9的出现次数,超过3次则以3次计,所以是四进制状态压缩
if (pos == 0)
return checkNum(now);
if (!limit && ~dp[pos][sta])
return dp[pos][sta];
int res = 0;
int up = limit ? num[pos] : 9;
for (int i = 0; i <= up; i++)
{
cnt[i]++;
res += dfs(pos - 1, limit && i == up,cnt[i]<=3?sta+(1<<(i<<1)):sta, now * 10 + i);
cnt[i]--;
}
if (!limit)
dp[pos][sta] = res;
return res;
}
int solve(int x)
{
if (x < 0)
return 0;
int bit = 0;
while (x)
{
num[++bit] = x % 10;
x /= 10;
}
return dfs(bit, true, 0, 0);
}
int main()
{
memset(dp, -1, sizeof(dp));
int t;
scanf("%d", &t);
while (t--)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", solve(r) - solve(l - 1));
}
return 0;
}