数字子序列和模m最大值问题

问题描述:

给定一个非负数组arr,和一个正数m。
返回arr的所有子序列中累加和 %m 之后的最大值。

  1. 如果arr中每个数字不大,怎么做?
  2. 如果arr中 m 的值很小,怎么做?
  3. 如果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表可以如下建法:
pic1
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的长度比较小,怎么做?

这里给出一个具体的要求:
pic
在线评测系统(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";
}

结果:
pic3
具体问题,具体分析。

纸上得来终觉浅,绝知此事要躬行。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值