EOJ:Ugly Numbers
——给一串数字。要求在数字中(第一个数字前不能添加)添加“+”或者“-”使得最后算出来的结果能够被2,3,5,7其中的一个整除。求这样的添加方案共有多少种。
——DP
——http://202.120.106.94/onlinejudge/problemshow.php?pro_id=426
——————————————————————————————————————————————————————————
拿到题首先的想法是搜索……一看这个复杂度果断就放弃了。
在做DP之前有个很重要的想法就是对结果取余,直接保存余数即可。
对几取余?
直观的想法是对2,3,5,7以及两两,三三,四个的公倍数分别取余,然后用容斥原理来做
但是仔细一想,只需要对210取余即可。最后的结果即为余数是0,2,4,6,8,……和3,6,9,12……5,10,15……7,14,21……这些相加
这样是不会有重复计算的,因为被210除余2和被210除余6的数肯定不是一个数。
——————————————————————————————————————————————————————————
首先因为添加的是加减号而没有乘除,所以不需要考虑优先级的问题,因此可以从左往右直接DP。
——————————————————————————————————————————————————————
之前想到的一个错误的DP方程:
DP[i][j]=Segma{DP[i-1][(j-str[i])%210]+DP[i-1][(j+str[i])%210]+DP[i-1][(j*10+str[i])%210]}
简单说就是一位一位的做,每一位可以添加加号减号或者不添加符号。
问题出在不添加符号,如果第I位之前的结果是31的话,在第I-1位与第I位之间不添加符号,按上面方程就是31*10+STR[I],但实际上31可能是由20+11得来的。那么不填符号应该是20+11*10+STR[I]才对。
——————————————————————————————————————————————————————————————————
正确的转移方程:
DP[I][J]表示字符串从0到I-1为止,这么多个数字添加了符号以后被210除余J的方案数。
X为从K到I-1字符串中的数字的值。
DP[I][J]=Segma{DP[K][J-X]+DP[K][J+X]} 0<=K<I
J-X就是添加了负号,J+X添加的是正号
#include<stdio.h> #include<string.h> #define SIZE 45 long long dp[SIZE][210]; bool flag[210]; long long ans; int len, n; char str[SIZE]; void solve() { int i, j; long long k, tmp; memset(dp, 0, sizeof(dp)); dp[0][0] = 1; for (i = 1; i <= len; i++) for (j = 0; j < i; j++) { tmp = 0; for (k = j; k < i; k++) tmp = (tmp * 10 + str[k] - '0') % 210; for (k = 0; k < 210; k++) { dp[i][(k + tmp) % 210] += dp[j][k]; if (j) dp[i][(k - tmp + 210) % 210] += dp[j][k]; } } memset(flag, 0, sizeof(flag)); for (i = 0; i < 210; i += 2) flag[i] = true; for (i = 0; i < 210; i += 3) flag[i] = true; for (i = 0; i < 210; i += 5) flag[i] = true; for (i = 0; i < 210; i += 7) flag[i] = true; ans = 0; for (i = 0; i < 210; i++) if (flag[i]) ans += dp[len][i]; } int main() { int i; scanf("%d", &n); getchar(); for (i = 0; i < n; i++) { scanf("%s", str); len = strlen(str); solve(); printf("Case #%d: %lld\n", i + 1, ans); } return 0; }