题目见https://www.geeksforgeeks.org/count-of-ways-to-split-a-given-number-into-prime-segments/。大概就是把一个全是数字的string任意分割成数字,并要求这些被分出来的数字都是质数,求一共有多少种组合。
作为一个不懂递归不懂dp的,做这题真是太难太痛苦了QAQ
首先先把判断质数的方法单独拎出来说,因为这道题说了分出来的数字比10^6小,所以我们其实可以先用Eratosthenes筛子法把所有1 - 10^6每个数字是否为奇数给cache下来形成一个boolean数组,这样判断分出来的数字是否是prime的时候就很方便的O(1)了。
private static int HIGH = 1000000;
private static int MOD = 1000000007;
private static boolean[] seive = new boolean[HIGH];
public static void buildSeive() {
Arrays.fill(seive, true);
seive[0] = false;
seive[1] = false;
for (int i = 2; i < HIGH; i++) {
if (seive[i]) {
for (int j = 2; i * j < HIGH; j++) {
seive[i * j] = false;
}
}
}
}
public static boolean isPrime(int num) {
return seive[num];
}
接下来就是重头戏之递归和dp。
首先是gfg上提供的递归解法。假设string的长度为n,递归的主要思想就是我们可以把这个string分成两部分,[0, i - 1]和[i, n -1],如果[i, n-1]是个prime的话我们就递归调用这个函数,看看[0, i - 1]可以被分成多少,最后一直递归到[0, 0]的时候base case直接return 1,表示现在这是个有效的解(这里没有完全搞透)。
那么如何把这个思想转换成代码,首先要写个recursion函数,根据上面的思想,函数参数需要传入string和当前要判断的部分的last index。写完base case以后就要开始划重点了。首先声明一个count变量来记录个数。split的时候因为可以在任意位置split(但是因为题目给了数字最多10^6,所以相当于至少都要在倒数第6位之后split),所以我们需要写一个for循环从1到6,在这个for循环中看看从string末尾到当前遍历到的这个位置(也就是上文说的[i, n - 1])是否是prime(当然还要做好boundary check,以及注意不能以0开头)。如果是prime的话,count就要加上对前半部分递归调用这个函数产生的结果,因为题目还要求mod了所以就再mod一下就好。时间复杂度O(n^2),空间O(n)。(虽然没想明白orz)
public static int stringPrime0(String inputString) {
buildSeive();
return (naiveRecursion(inputString, inputString.length()));
}
public static int naiveRecursion(String inputString, int index) {
if (index == 0) {
return 1;
}
int count = 0;
for (int j = 1; j <= 6; j++) {
if (index - j >= 0 && inputString.charAt(index - j) != '0' &&
isPrime(Integer.parseInt(inputString.substring(index - j, index)))) {
count += naiveRecursion(inputString, index - j);
count %= MOD;
}
}
return count;
}
然后由于递归会出现重复计算,于是就可以memoization一下,也不是什么高大上的东西,就是多pass个数组来存当前结果,如果已经计算过的话就直接从数组里return就好……只是需要注意,数组最开始初始化的时候要初始化成-1,以免跟真正的结果撞了导致直接return,以及开的数组大小要是len + 1,因为最开始调用的时候传入的是len。
public static int stringPrime1(String inputString) {
buildSeive();
int len = inputString.length();
int[] memo = new int[len + 1];
Arrays.fill(memo, -1);
memo[0] = 1;
return memoizationRecursion(inputString, len, memo);
}
public static int memoizationRecursion(String inputString, int index, int[] memo) {
if (memo[index] != -1) {
return memo[index];
}
int count = 0;
for (int j = 1; j <= 6; j++) {
if (index - j >= 0 && inputString.charAt(index - j) != '0' &&
isPrime(Integer.parseInt(inputString.substring(index - j, index)))) {
count += memoizationRecursion(inputString, index - j, memo);
count %= MOD;
}
}
memo[index] = count;
return count;
}
然后就是dp了,dp研究了老半天才想明白,借鉴了https://stackoverflow.com/questions/59407508/how-to-count-the-number-of-ways-a-given-string-can-be-split-into-prime-numbers,但刚开始从py翻译成java总搞不对。跟递归很像但是又有点不一样。前面的递归是从后往前做的,dp因为要依据前面的计算结果所以感觉从前往后做比较顺一些。dp的整体思路在于,首先我们要外层循环i把整个string遍历一遍(类似于递归里面的index参数),内层循环j把1-6遍历一遍,用dp[i]表示以i结尾的字符串有多少种分法。和递归类似,如果[i - j, i]是个prime,那么dp[i]就要加上前半部分的分法数量,即dp[i - j]。表达式大概是dp[i] = sum(dp[i - j]) for (i - 6 <= j < i) if dp[i - j] > 0 && isPrime(input[i - j, i])。写成代码的话需要把dp[0]初始化成1,代表[0, i]这整个string都是个prime:
public static int stringPrime(String inputString) {
buildSeive();
int len = inputString.length();
int[] dp = new int[len + 1];
dp[0] = 1;
for (int i = 1; i <= len; i++) {
int count = 0;
for (int j = 1; j <= 6; j++) {
if (i - j >= 0 && inputString.charAt(i - j) != '0' &&
isPrime(Integer.parseInt(inputString.substring(i - j, i)))) {
count += dp[i - j];
}
}
dp[i] = count;
}
return dp[len];
}
刚刚终于搞明白stackoverflow上的那个解法,和我自己的想法有一点点不一样,就是在已经知道当前[i - j, i]是个prime的时候,直接加上dp[i - j],然后最后再加上判断当前[0, i]这整个string是否是prime,如果是的话就dp[i]++,好像也就相当于把我的dp[0] = 1的部分变成了后面这个判断?嗯是的,又想了想感觉好像还是这个方法更直观一点,不需要前面特意搞dp[0],哎,还是我太菜了。
public static int stringPrimeFromStackOverflow(String inputString) {
buildSeive();
int len = inputString.length();
int[] dp = new int[len + 1];
for (int i = 1; i <= len; i++) {
int count = 0;
for (int j = 1; j <= 6; j++) {
if (i - j >= 0 && inputString.charAt(i - j) != '0' &&
isPrime(Integer.parseInt(inputString.substring(i - j, i)))) {
count += dp[i - j];
}
}
dp[i] = count;
if (isPrime(Integer.parseInt(inputString.substring(0, i)))) {
dp[i]++;
}
}
return dp[len];