学习笔记2.2续

例题:多重部分和问题

定义:dp[i+1][j]前i种数字是否能构成j
为了用前i种数字加和成j,也就需要能用前i-1种数字加和成j,j-ai,···,j-mi x ai中的某一种。我们可以定义如下递推关系:
dp[i+1][k] (0<=k<=mi且k X ai<=j时存在dp[i][j-k X ai]为真的k)

int n,K,a[maxn],m[maxn];
bool dp[maxn+1][maxk+1];
void solve(){
	dp[0][0]=true;
	for(int i=0;i<n;i++){
		for(int j=0;j<=K;j++){
			for(int k=0;k<=m[i]&&k*a[i]<=j;k++){
				dp[i+1][j]=dp[i][j]||dp[i][j-k*a[i]]
			}
		}
	}
}

这个算法的复杂度是O(KΣimi),这样并不够好。一般用DP求bool结果的话会有不少浪费,同样的复杂度通常能获得更多的信息。在这个问题中,我们不光求出能否得到目标的和数,同时把得到时ai这个数还剩下多少个计算出来,这样就能减少复杂度
dp[i+1][j]:=用前i种数加和得到j时第i种数最多能剩余多少个(不能得到的话为-1)
按照如上所述定义递推关系,这样如果前i-1个数加和能得到j的话,第i个数就可以留下mi个。此外,前i种数加和出j-ai时第i种数还剩下k的话(k>0)用这i种数加和到j时第i种数就能剩下k-1个。由此我们可以得到下面的递推式
dp[i+1][j]=mi(dp[i][j]>=0)||-1(j<ai或者dp[i+1][j-ai]<=0)||dp[i+1][j-ai]-1
这样,只要看最终是否满足dp[n][K]>=0就可以知道答案了
时间复杂度为O(nK)

int dp[maxk+1];
void solve(){
	memset(dp,-1,sizeof(dp));
	dp[0]=0;
	for(int i=0;i<n;i++){
		for(int j=0;j<=K;j++){
			if(dp[j]>=0){
				dp[j]=m[i];
			}
			else if(j<a[i]||dp[j-a[i]]<=0){
				dp[j]=-1;
			}else{
				dp[j]=d[j-a[i]]-1;
			}
		}
	}
	if(dp[K]>=0)printf("Yes");
	else cout << "No" << endl;
}

总结:如果前i个数字已经能够组成数字j,那么在当前状态d[j]我就可以标记为m[i],即一个都不用。如果没有组成j并且j<a[i]或者a[i]并不能组成j-a[i] (或者组成a[i]之后就把次数用光了)那么就把当前状态置为-1。否则就在原来j-a[i]的基础上-1。

例题:最长上升子序列

这个问题是被称作最长上升子序列(LIS,Longest Increasing Subsequence)的著名问题。这一问题通过使用DP也能很有效率地求解。
定义:dp[i]:以ai为末尾地最长上升子序列的长度。
以ai结尾的上升子序列是
1.只包含ai的子序列
2.在满足j<i并且aj<ai的以aj为结尾的上升子列末尾,追加上ai后得到的子序列

这二者之一,这样就能得到如下递推关系:
dp[i]={1,dp[j]+1|(j<i并且a[j]<a[i])}
使用这一递推公式就可以在O(n2)时间内解决这个问题。

int n;
int a[maxn];
int dp[maxn];
void solve(){
	int res=0;
	for(int i=0;i<n;i++){
		dp[i]=1;
		for(int j=0;j<i;j++){
			if(a[j]<a[i]){
				dp[i]=max(dp[i],dp[j]+1);//如果直接dp[i]=dp[j]+1的话 3 4 1 7这样的序列,在迭代到7时就会变成2而不是3
			}
		}
		res=max(res,dp[i]);
	}
	cout << res << endl;
}

此外还可以定义其他的递推关系。前面我们利用DP求取正对最末尾的元素的最长子序列。如果子序列的长度相同,那么最末尾的元素较小的在之后会更加有优势,所以我们再反过来用DP针对相同长度情况下最小的末尾元素进行求解。
dp[i]:=长度为i+1的上升子序列中末尾元素的最小值(不存在则为INF)
我们来看看如何用DP来更新这个数组。
最开始全部dp[i]的值都初始化为INF。然后有前到后逐个考虑数组的元素,对于每个aj,如果i=0或者或者dp[i-1]<aj的话就用dp[i]=min(dp[i],aj)进行更新。最终找出使得dp[i]<INF的最大的i+1就是结果了。这个DP直接实现的话,能够与前面的方法一样在O(n2)的时间内给出结果,但这一算法还可以
进行进一步优化。首先dp数组中除INF之外都是单调递增的,所以可以知道对于每个aj最多只需要一次更新。对于这次更新究竟应该在什么位置,不必逐个遍历,可以利用二分搜索,这样就可以在O(nlogn)时间内求出结果

int dp[maxn];
void solve(){
	fill(dp,dp+n,INF);
	for(int i=0;i<n;i++){
		*lower_bound(dp,dp+n,a[i])=a[i];
	}
	cout << lower_bound(dp,dp+n,INF)-dp << endl;
}

对于这个算法以样例{4,2,3,1,5}为例,它真正的上升子序列应该为{2,3,5},长度为3。但是如果我们用上面的算法进行求解之后,长度虽然也为3,但是dp内记录的上升子序列却时{1,3,5},显然这并不是我们要求的最长上升子序列。但是!要注意一点,我们这里只求最长的上升子序列的长度,我们本质要求的是前面的数比后面的数小就行了,并不要求将其序列展现出来。所以第一个数为1还是2无关紧要,因为它们都比3要小。当前算法遍历一遍数组a,如果不考虑每个实数的话,其实就是每次都将大的数添加到dp数组的最后(当然小的数被更新到前面了,但是不影响我们的结果),最后求出的不是INF的个数就是我们最终的长度。

专栏 lower_bound
这个函数从已排好序的序列a中利用二分搜索找出指向满足ai>=k的ai的最小的指针。类似的函数还有upper_bound,这一函数求出的是满足ai>k的ai的最小的指针。例:求出数组a中k的个数upper_bound(a,a+n,k)-lower_bound(a,a+n,k);

2.3.3 有关计数问题的DP

例题1:划分数

挑战程序设计竞赛 page66

这样的划分被称作n的m划分,特别的,m=n时称作n的划分数。DP不仅对于求解最优问题有效,对于各种排列组合的个数、概率或者期望之类的计算同样很有用。在此,我们定义如下:
dp[i][j]=j的i划分的总数(dp[m][n]就是代表n的m划分)
将j分划成i个的话,可以先取出k个,然后将剩下的j-k个分成i-1份,就可以得到下面的递推式dp[i][j]=Σjk=0dp[i-1][j-k]。然而这个这个递推式是不正确的,用这个办法的话,例如1+1+2和1+2+1的划分就当作不同的划分来计数了。所以我们要考虑其他的递推关系。
考虑n的m划分aimi=1ai=n),如果对于每个i都有ai>0,那么{ai-1}就对应了n-m的m划分(如果每个ai>0那么n的m划分就是刚好有m组,每个都-1的话那么就是n-m的m划分)。另外,如果ai=0那么就对应了n的m-1划分。综上我们就有了如下递推关系。
dp[i][j]=dp[i][j-i]+dp[i-1][j] (刚好凑满的加上没有凑满的)
这个递推式可以不重复地计算所有地划分,复杂度为O(nm)。

int n,m,M;//输入
int dp[maxm+1][maxn+1];//dp数组
void solve(){
	dp[0][0]=1;//0的0划分自然是1
	for(int i=1;i<=m;i++){
		for(int j=0;j<=n;j++){//dp[1][0]0的1划分当时是1
			if(j>=i){
				dp[i][j]=(dp[i][j-i]+dp[i-1][j])%M;
			}
			else{
				dp[i][j]=dp[i-1][j];
			}
		}
	}
}

多重集组合数

为了不重复技术,同一种类的物品最好一次性处理好。于是我们按照下面方式进行定义。
dp[i+1][j]:=从前i种物品中取出j个的组合总数
为了从前i种物品中取出j个,可以从前i-1种物品中取出j-k个,再从第i种物品中取出k个添加进来,所以有如下递推关系:
dp[i+1][j]=Σmin(j,a[i])k=0dp[i][j-k]
直接计算这个递推关系的话复杂度是O(nm2),不过因为我们有Σmin(j,a[i])k=0dp[i][j-k]=Σmin(j-1,a[i])k=0dp[i][j-1-k]+dp[i][j]-dp[i]j-1-ai(前i种能凑出j-1种的加上前i-1种就能凑出j种的再减去前i-1种不能凑出a[i]种的)(dp[i+1][j-1]代表了我前i种能凑齐j-1个数,其中包括了也能凑齐dp[i+1][j]的数和不能凑齐dp[i+1][j]的数而不能凑齐dp[i+1][j]的数就是能凑齐dp[i][j-1-a[i]]的数)

int n,m;
int a[maxn];
int dp[maxn+1][maxm+1];
void solve(){
	for(int i=0;i<=n;i++){
		dp[i][0]=1;//每次一个都不取的方法只有一种
	}
	for(int i=0;i<n;i++){
		for(int j=1;j<=m;j++){
			if(j-1-a[i]>=0){
				dp[i+1][j]=dp[i+1][j-1]+dp[i][j]-dp[j-1-a[i]];
			}else dp[i+1][j]=dp[i+1][j-1]+dp[i][j];
		}
	}
	cout << dp[n][m] << endl;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值