问题描述:
给定一个非负数组arr,和一个正数m。
返回arr的所有子序列中累加和 %m 之后的最大值。
- 如果arr中每个数字不大,怎么做?
- 如果arr中 m 的值很小,怎么做?
- 如果arr的长度很短,但是arr每个数字比较大并且m比较大呢?
1. 暴力解法( O(N^2) )
所有子序列的和模上m的值全部求出来,求出最大的即可。
code:
/** 生成每个子序列和的递归函数
* @param arr: 原始数组名
* @param n: 原始数组长度
* @param aux_arr: 辅助数组名,每次求得的子序列和的值放在辅助数组里面
* @param index: 等价于二叉树的层数
* @param k: aux_arr数组的遍历位置
* @param sum: 记录的和
*/
int process(int* arr, int* aux_arr, int index, long& k, int sum, int n) {
if (index == n) {
aux_arr[k++] = sum;
}
else {
process(arr, aux_arr, index + 1, k, sum, n); //用递归解分治问题
process(arr, aux_arr, index + 1, k, sum + arr[index], n);
}
}
/** 暴力解法 分治问题
* @param arr: 数组名
* @param n: 数组长度 n 不能太大,2^32 就已经是uint的极限了
* @param m: 模数
*/
int solution1(int* arr, int n, int m) {
if (n <= 0)
return 0;
int* aux_arr = new int[(long)pow(2, n)];
long k = 0;
process(arr, aux_arr, 0, k, 0, n);
int maxValue = 0;
for (long i = 0; i < (long)pow(2, n); ++i) {
maxValue = std::max(maxValue, aux_arr[i] % m);
}
delete[] aux_arr;
return maxValue;
}
2. 动态规划
这是一个类背包问题,DP类型是从左往右的(一共有四种类型)。
同时,这里有三小问,每一小问的参数取值不同,所以肯定需要采用不同的解法,这就是所谓的看菜吃饭。
1)如果arr中每个数字不大,怎么做?
arr中每个数字不大,那么数组和sum就不大,二维dp表可以如下建法:
code:
// functor
struct myclass {
int operator() (int i, int j) {
return i + j;
}
};
/** 动态规划解法(二维dp) 对应第一小问:arr中每个数字不大的情况
* @param arr: 数组名
* @param n: 数组长度
* @param m: 模数
*/
int solution2(int* arr, int n, int m) {
if (n <= 0)
return 0;
//求数组和
int sum = std::accumulate(arr, arr + n, 0, myclass());
bool** dp = new bool*[n];
for (int i = 0; i < n; ++i) {
dp[i] = new bool[sum + 1];
}
// initialize
for (int i = 0; i < n; ++i) {
for (int j = 0; j < sum + 1; ++j) {
dp[i][j] = false;
}
}
for (int i = 0; i < n; ++i) {
dp[i][0] = true;
}
dp[0][arr[0]] = true;
for (int i = 1; i < n; ++i) {
for (int j = 1; j <= sum; ++j) {
dp[i][j] = dp[i - 1][j];
if (j - arr[i] >= 0) {
dp[i][j] = dp[i][j] | dp[i - 1][j - arr[i]];
}
}
}
int ans = 0;
// 最后遍历最后一行就好了
for (int i = 0; i <= sum; ++i) {
if (dp[n - 1][i])
ans = std::max(ans, i % m);
}
// free memory
for (int i = 0; i < n; ++i) {
delete[] dp[i];
}
delete[] dp;
return ans;
}
2)如果arr的sum比较大,而arr中 m 的值很小,怎么做?
也是建个二维dp表,但是是以m来建, dp[n][m]。
code:
/** 动态规划解法(二维dp) 对应第二小问:arr中每个数字比较大,而 m 比较小的情况
* @param arr: 数组名
* @param n: 数组长度
* @param m: 模数
*/
int solution3(int* arr, int n, int m) {
if (n <= 0)
return 0;
bool** dp = new bool*[n];
for (int i = 0; i < n; ++i) {
dp[i] = new bool[m];
}
// initialize
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
dp[i][j] = false;
}
}
for (int i = 0; i < n; ++i) {
dp[i][0] = true;
}
dp[0][arr[0] % m] = true;
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
dp[i][j] = dp[i - 1][j];
int cur = arr[i] % m;
if (j - cur >= 0) { // 4 7 3即可 m = 9
dp[i][j] = dp[i][j] | dp[i - 1][j - cur];
}
else { // 8 3 m = 9 4即可
dp[i][j] = dp[i][j] | dp[i - 1][j + m -cur];
}
}
}
int ans = 0;
// 最后遍历最后一行就好了
for (int i = 0; i < m; ++i) {
if (dp[n - 1][i])
ans = i;
}
// free memory
for (int i = 0; i < n; ++i) {
delete[] dp[i];
}
delete[] dp;
return ans;
}
2)如果arr的sum和m都比较大,而arr的长度比较小,怎么做?
这里给出一个具体的要求:
在线评测系统(OJ)一般有要求 C/C++ 1s,python/Java 2-3s,一般C语言1s对应的常数操作为 108~109。
所以针对上面图示具体要求,对整个数组暴力肯定超时,那么可以将数组分半,分别去求mod完后的值,然后合并。
具体见code:
// 在arr[index, end]上自由选择,每一种选择出来的累加和 mod m 的结果
// 放到set中去
void process(int* arr, int index, int sum, int end, int m, std::set<int>& s) {
if (index == end + 1)
s.insert(sum % m);
else {
process(arr, index + 1, sum, end, m, s);
process(arr, index + 1, sum + arr[index], end, m, s);
}
}
/** 第三小问,如果arr的累加和很大,并且m也很大,构造二维dp表显然不达要求
* 但是arr的长度不大,意味着暴力解法有希望 (2^n) ,如果 30 < n < 35,
* 那么整体暴力显然不行,那么可以拆成左右两部分,分别去做,然后合并即可。
*/
int solution4(int* arr, int n, int m) {
if (n <= 0)
return 0;
int mid = n / 2;
std::set<int> set1;
process(arr, 0, 0, mid, m, set1);
std::set<int> set2;
process(arr, mid + 1, 0, n - 1, m, set2);
int ans = 0;
for (int num : set1) {
// 这里找 (num + ?) % m 使得最大,? 是 <= m-1-num 的最近的那个。
int tmp = m - 1 - num;
while (tmp >= 0) {
if (set2.find(tmp) != set2.end()) {
break;
}
tmp--;
}
ans = std::max(ans, num + tmp);
}
return ans;
}
四种方法,一起比较测试,如果有一点错误,屎都给你测出来了。
具体code:
// for test
void test() {
std::cout << "test begin. \n\n";
int testTime = 100;
while (testTime--) {
srand((unsigned)time(NULL));
int arrLen = rand() % 10 + 20; // [20, 29]
int* arr = new int[arrLen];
for (int i = 0; i < arrLen; ++i) {
arr[i] = rand() % 100 + 100; // [100, 199]
}
int m = rand() % 100 + 1; // [1, 100]
if (solution2(arr, arrLen, m) != solution3(arr, arrLen, m) || \
solution1(arr, arrLen, m) != solution2(arr, arrLen, m) || \
solution3(arr, arrLen, m) != solution4(arr, arrLen, m)) {
std::cout << "Oops!\n";
}
delete[] arr;
}
std::cout << "test finish. \n\n";
}
结果:
具体问题,具体分析。
纸上得来终觉浅,绝知此事要躬行。