k个数相加和为m的种数

19 篇文章 0 订阅
8 篇文章 0 订阅

问题I:

盒子中有n张卡片,上面的数字分别为k1,k2,...,kn。你有4次机会,每抽一次,记录下卡片上的数字,再将卡片放回盒子中。如果4个数字的和等于m。则你就赢得游戏,否则就是输。直觉上,赢的可能性太低了。请你给出程序,判断是否有赢的可能性。

分析:

要求的是能够赢得游戏概率,很明显,若能够求出4个数子和为m的种数(记作T),那么赢的概率就为T/n^4。

这里,将核心部分扩展为问题II。


问题II:

给出n个正整数,从中依次选择k个数,使得和等于m。问:满足要求的选择的种类数。

这里:同一个数字可以使用多次。顺序不一样,种类也不一样。

例如:集合为:{1,2,3,4,5},k=3,m=4。就有3种选择方式:1+1+2, 1+2+1, 2+1+1。


分析I:

此问题可以暴力枚举解决,但是时间复杂度就是O(n^k),指数级别。


要求k个数字的和为m的种数,很容易想到了“背包问题”。只是这里,对背包里物品的数量做出了限制,而且对物品的放入顺序也要做出区分。

想到一下解法:

若用f[ki][ni][mi]表示用前ni个数字中选出ki个,和为mi的种数。那么:

f[ki][ni][mi] = f[ki][ni-1][mi] + f[ki-1][ni][mi-v[ni]]

最后的种数即为f[k][n][m]。

但这个解法是错误的,

原因:考虑数字的可重复性,但没有考虑数字的顺序性。不能简单地加上 f[ki-1][ni][mi-v[ni]],因为第ni个数字的位置是不确定的。


上面的状态转移方程,把问题想复杂了,其实可以用更简单的动态规划结构来求解:

用f[ki][mi]表示用ki个数字得到和为mi的种数。那么:

f[ki][mi] = Σf[ki-1][mi-v[j]]。j=1,2,3...,n。

时间复杂度:O(kmn), 空间复杂度O(km)

代码:

int getCount(const vector<int> &v, int m, int k){  
	int n = v.size();
	vector<vector<int> > sum(k+1, vector<int>(m+1, 0));
	sum[0][0] = 1;
	for(int ki = 1; ki <= k; ++ki){
			for(int mi = 1; mi <= m; ++mi)
				for(int ni = 1; ni <= n; ++ni)
					sum[ki][mi] += mi >= v[ni-1] ? sum[ki-1][mi-v[ni-1]] : 0;
	}
	return sum[k][m];
}

测试代码:

int main() {
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	vector<int> v(begin(arr), end(arr));
	int n = 5, k = 4;
	int count = getCount(v, n, k);
	cout << count << " " << count / pow(n, k) << endl;
	return 0;
}

补充:

上面的实现,可用仿照背包问题,逆序遍历,来减少空间复杂度。

新的实现:时间复杂度:O(kmn), 空间复杂度O(m)

int getCount2(const vector<int> &v, int m, int k){  
	int n = v.size();
	vector<int> sum(m+1, 0);
	for(int ki = 1; ki <= k; ++ki){
		sum[0] = ki == 1 ? 1 : 0;
		for(int mi = m; mi >= 0; --mi){
			sum[mi] = 0;
			for(int ni = 1; ni <= n; ++ni)
				sum[mi] += mi >= v[ni-1] ? sum[mi-v[ni-1]] : 0;
		}
	}
	return sum[m];
}


分析II:

from:@陈利人

这个题目最直接的方法就是四重循环遍历,n^4的时间复杂度,比较恐怖,但确实一个很好的起点。不用觉得很丢人,只要我们持续改进即可。
假设a,b,c是k1到kn中的三个数字,是否存在d使得,a+b+c+d=m?d在k1到kn中。和题目中的意思是一样的,变换等式如下:
d = m - a - b - c
如果满足上式,就是说,要在k1到k2中查找d,即:查找m - a - b - c,找到就存在;找不到,就是不存在。查找有线性查找,二分查找等。四重循环最内一层循环,可以认为是线性的查找,如果想更快一些,可以应用二分查找,而二分查找需要k1到kn是排序的,则先对n个数字进行排序,时间复杂度O(nlogn)。然后最内一层循环,改为二分查找,时间复杂度为O(n^3logn)。所以总的时间复杂度为O(n^3logn)。

经过上面的分析,我们可以得到启发,既然可以提出一个d,那么就可以再提出一个c。将a+b+c+d=m转换为c+d=m-a-b。这样,我们可以枚举k1到kn的两个数的和,然后对n2个和,进行排序,时间复杂度为o(n^2logn)。然后遍历n^2个和,设每一个和为pair,查找是否存在m-pair,同样二分查找,时间复杂度为O(n^2logn)。总的时间复杂度为O(n^2logn)。


小结:

分析I的算法更直观一点,分析II的算法更巧妙。

分析I的算法适用于K比较大的情况下,分析II的算法适用与K比较小的的情况下,可以互补。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值