数组arr的任意子序列和模m的最大值是多少?
提示:dp在填表时一定考虑时间复杂度
题目
数组arr,其元素arr[i]>=0,arr的任意子序列和为sum,给定一个数m,sum%m 的最大值是多少?
一、审题
示例:比如 1 3 2 4 = arr, m=5;求任意子序列和sum%m的max值为多少?
可以暴力思考:
任意位置i的数,不加到sum中,则sum=0,sum%m=0
单独只要arr[i],则:sum%m=1 3 2 4
1+3=4,sum%m=4%4=0
1+3+2=6,sum%m=6%4=2
1+3+2+4=10,sum%m=10%4=2
3+2=5,sum%m=5%4=1
3+2+4=9,sum%m=9%4=1
2+4=6,sum%m=6%4=2
因此,所有sum%m的最大值是4;
这就是题意
二、解题
笔试分治法AC简单,o(2^(N/2)) << o(2**N)
分治法的目的就是将一个子算法的运算规模大大降低!
将arr分为N/2的两部分:左边left,右边right
左边最大值max1,右边最大值max2
很显然,任意子序列的和sum%m取值范围都在0–m-1之间
而最终的max很可能只来自于left,也可能只来自于right,也可能是2着的组合【大概率】
而左右两边再怎么融合最大值,也有限制,即max<=m-1
故可以取左边一个元素x,然后去右边找<=m-1-x那个值,拿过来加起来,就是max
理解?
现在,单独计算一个部分arr的max自序和,直接暴力枚举每一个位置index处的arr[index]要?还是不要,求出来的值,放入一个有序表,重复的干掉,省得浪费空间:
这个代码是dp中排列组合,再简单不过的代码了
//暴力递归枚举一个位置i,要不要,从i---end位置上自由选择的sum%m放入有序表中
public static void process(int[] arr, int m, int i, int end, int sum, TreeSet<Integer> set){
if (i == end + 1) set.add(sum % m);//暴力决定到end+1,说明i--end都决定好了,和已经搞定了
else {
//sum要arri还是不要呢
process(arr, m, i + 1, end, sum, set);//不要arri
process(arr, m, i + 1, end, sum + arr[i], set);//要arri
}
}
然后,笔试算法AC分治法大流程为:
先求左边sum%m的所有可能取值
再求右边sum%m的所有可能取值
最后融合左右,取max的sum%m
public static int maxModValue3(int[] arr, int m){
if (arr == null || arr.length == 0) return 0;
if (arr.length == 1) return arr[0] % m;
int N = arr.length;
int mid = (N - 1) >> 1;
TreeSet<Integer> setLeft = new TreeSet<>();//升序
TreeSet<Integer> setRight = new TreeSet<>();//升序
process(arr, m, 0, mid, 0, setLeft);
process(arr, m, mid + 1, N - 1, 0, setRight);//左右收集枚举的结果
int max = 0;
for (Integer left:setLeft) max = Math.max(max, left + setRight.floor(m - 1 - left));
//由于setLeft一定存在0,setRight也一定存在0,所以,当
//left出0,右边就只剩下那个最接近m-1那个值是可以的
//left出m - 1,右边就只剩下那个最接近0那个值是可以的
//当left出left时,我们右边一定是找那个接近于m-1-lfet那个值
//目标就是搞出最接近m-1的这个max值来,所以一句话枚举了三种情况
面试DP方法1,o(N*maxSum),如果maxSum不大的话
定义dp[i][j]这样的含义:
从0–i位置上任选数,恰好组成sum放入j,能组成吗?能就是true,否则就是false
也就是说j0—maxSum,这个maxSum要是不大,还可以这么玩
我们需要在N-1行上【代表0–N-1上任选数】,找所有的j,当dp[N-1][j] 为true时,让j%m取最大值,
这个方法,有点蠢,但是就这个思想,可行,填一个表即可
o(N*maxSum) 显然maxSum很大,那你这个填表复杂度过高
看图:
第0列,全部元素都不要,sum=0,故,true
第0行,得看jarr[0]吗,就0号为arr[0]最多组成一个j
其余的dp[i][j]:
看dp[i-1][j]啥情况,然后现在,i位置,选择要i还是不要i,两者是或关系
代码:
//也就是说,不先取%,而是搞出所有sum来,复杂度为o(N*sumMax),当sumMax过大就废了
//dpij怎么填,要i,和不要i,两种情况,算一种就行
public static int maxModValue(int[] arr, int m){
if (arr == null || arr.length == 0 || m <= 0) return -1;
int N = arr.length;
int sumMax = arr[0];
for (int i = 1; i < N; i++) {
sumMax += arr[i];//全体累加和
}
boolean[][] dp = new boolean[N][sumMax + 1];
//第一行,只需要看arr【0】是否能凑出j来
dp[0][arr[0]] = true;//其余全是false,就一个数怎么能凑别的累加和呢
//第一列,看看从0--i上能不能凑出j==0,当然可以,一个不要不就完了
for (int i = 0; i < N; i++) {
dp[i][0] = true;
}
//任意位置ij,看arri要不要
for (int i = 1; i < N; i++) {
for (int j = 1; j < sumMax; j++) {
dp[i][j] = dp[i - 1][j];//看看不要i时是否已经搞定了
//再看看要arri时,如何,要求j--arri那个地方已经在前面i-1中凑好了
if (j - arr[i] >= 0) dp[i][j] = dp[i][j] | dp[i - 1][j - arr[i]];
}
}
//然后看最后一个行,哪个取%之后最好
int max = 0;
for (int j = 0; j < sumMax; j++) {
if (dp[N - 1][j]) max = Math.max(max, j % m);
}
return max;
}
面试DP方法2,o(N*m),如果maxSum过大的话
既然maxSum可能太大,但是先%m的话,范围就限制到0–m-1了,大大降低了填表的麻烦
定义dp[i][j]这样的含义:
从0–i位置上任选数,恰好组成sum,然后将sum%m放入j,能组成吗?能就是true,否则就是false
第0列,全部元素都不要,sum=0,故全部都是true
第0行,得看j==arr[0]%m吗,其他都是false
其余的dp[i][j]:
看dp[i-1][j]啥情况,然后现在,i位置,选择要i还是不要i,两者是或关系
但是要注意,既然是%m了,那就要仔细了,如果要arr[i],你得观察
之前哪个x%m 刚刚好与现在的arr[i]%m能凑成m,这样才能算出来才j
(1)比如:m=5,arri=3,j=7时
要arri=3,则 j=7=sum%m=(x+3)%5=x%5+3%5
则x%5=j-3%5 那要求之前0–i-1上有一个和为x%5=4,也即我们需要看dp[i-1][j-arri%m]
这里要保证j - arr[i] % m >= 0
(2)又要注意:m=10,arri=3呢,j=2时
要arri=3,则 j=2=sum%m=(x+3)%10=**x%10+3%10=9+3 % 10 = 2 **
因为j永远在0–m-1之间,x=9怎么来的,x=9=2+m-arri=2+10-3=9
故我们需要看dp[i-1][j+m-arri%m]
而上面(1)(2)是互斥的,是(1)就不会是(2),因而在填表时要分开情况:
等填完表,从j=m-1–0索引,第一个true对应的j就是我们要的结果
代码:
public static int maxModValue2(int[] arr, int m){
if (arr == null || arr.length == 0 || m <= 0) return -1;
int N = arr.length;
boolean[][] dp = new boolean[N][m];
//第一行
dp[0][arr[0] % m] = true;//j是累加和%m哦,不是累加和
//第一列,全不要
for (int i = 0; i < N; i++) {
dp[i][0] = true;
}
//三种情况填dpij
for (int i = 1; i < N; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = dp[i - 1][j];//不用i
//用i,两者进一个
if (j - arr[i] % m >= 0) dp[i][j] = dp[i][j] | dp[i - 1][j - arr[i] % m];
else dp[i][j] = dp[i][j] | dp[i - 1][j + m - arr[i] % m];
}
}
int max = 0;
for (int j = m - 1; j >= 0; j--) {
if (dp[N - 1][j]) return j;//j就是取完模的
}
return max;
}
总结
提示:重要经验:
1)笔试分治法,降低运算规模,是最容易写的代码
2)sum先%m能降低填表格的复杂度