基础dp总结

基础dp总结

总结


POJ 1948 Triangular Pastures
用给出的N个篱笆围成三角形,求三角形的最大面积可以是多少(答案乘以100后再取整数部分输出)。

因为面积要最大,所以篱笆是肯定要用完的。所以用F[i][j][k]表示是否可以用前i个篱笆组成两条长度分别为j和k的边。那么另一边的长度自然就是sum-j-k。注意判断他们是否能围成三角形。
状态转移方程为:F[i][j][k]=(F[i-1][j-Fence[i]][k]||F[i-1][j][k-Fence[i]])。
小剪枝:j和k是等价的,我们可以假设j>=k.并且j的长度< sum/2;

HDU 4526 威威猫系列故事——拼车记
话说威威猫有一次去参加比赛,虽然学校离比赛地点不太远,但威威猫还是想坐出租车去。大学城的出租车总是比较另类,有“拼车”一说,也就是说,你一个人坐车去,还是一堆人一起,总共需要支付的钱是一样的(每辆出租上除司机外最多坐下4个人)。刚好那天同校的一群Acmer在校门口扎堆了,大家果断决定拼车去赛场。
  问题来了,一辆又一辆的出租车经过,但里面要么坐满了乘客,要么只剩下一两个座位,众Acmer都觉得坐上去太亏了,威威猫也是这么想的。
  假设N名Acmer准备拼车,此时为0时刻,从校门到目的地需要支付给出租车师傅D元(按车次算,不管里面坐了多少Acmer),假如S分钟后恰能赶上比赛,那么S分钟后经过校门口的出租车自然可以忽略不计了。现在给出在这S分钟当中经过校门的所有的K辆出租车先后到达校门口的时间Ti 及里面剩余的座位Zi (1 <= Zi <= 4),Acmer可以选择上车几个人(不能超过),当然,也可以选择上0个人,那就是不坐这辆车。
  俗话说,时间就是金钱,这里威威猫把每个Acmer在校门等待出租车的分钟数等同于花了相同多的钱(例如威威猫等待了20分钟,那相当于他额外花了20元钱)。
  在保证所有Acmer都能在比赛开始前到达比赛地点的情况下,聪明的你能计算出他们最少需要花多少元钱么?

dp[i][j]表示前i辆车送走j个acmer的最小花费,然后就有dp[i][j]=dp[i-1][j-k]+k*t+d;(t为车辆到达的时间,d为花费)
因为你等待的时间取决于每辆车到达的时间。所以阶段应该是第几辆车。

正整数分组
将一堆正整数分为2组,要求2组的和相差最小。
例如:1 2 3 4 5,将1 2 4分为1组,3 5分为1组,两组和相差1,是所有方案中相差最少的。
(N <= 100, 所有正整数的和 <= 10000)

直接分组是不行的,考虑选一组出来,使得这一组尽可能接近sum/2.所以就用类似背包的方法做就可以了。

2.一维变二维

最大子矩阵和
一个M*N的矩阵,矩阵中有一些整数(有正有负),找到此矩阵的一个子矩阵,并且这个子矩阵的元素的和是最大的,输出这个最大的值。(N<=500)

第一想法dp是N4的。可我们需要N3的。考虑如果此题是一维的,我们怎么做(即最大子段和)。
dp[i]=max(a[i],a[i]+dp[i-1],0)
所以我们可以枚举左右两个边界,从上到下做一维的最大子段和。因为枚举了边界[l,r],所以每次可能加上的a[i]是g[i][l->r],所以需要预处理。

3.序列变环

循环数组最大子段和
N个整数组成的循环序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的连续的子段和的最大值(循环序列是指n个数围成一个圈,因此需要考虑a[n-1],a[n],a[1],a[2]这样的序列)。当所给的整数均为负数时和为0。例如:-2,11,-4,13,-5,-2,和最大的子段为:11,-4,13。和为20。

这道题的方法貌似不普遍适用。由于这是一个环,从中取出一段最大的子段后,剩下的必是最小的子段。所以从任意处断开,同时跑最大子段和及最小子段和,最后,取max(最大值,sum-最小值)即可。
一般的方法是:

1.断环为链,倍长。
2.在任意处断开跑第一次,第二次将断开处强制连接。

4.各类背包

01背包
最简单的二维枚举

注意压成一维后,需要倒序枚举。

完全背包
每种物品无限多的背包

压成一维后,要顺序枚举

多重背包
每件物品有K个

考虑将k进行二进制分解。此处与快速幂不同每次枚举1,2,4,8…如果当前不够了,就把当前所有的当成一个物品做01背包。

5.重复相减

子序列的个数
给定一个正整数序列,序列中元素的个数和元素值大小都不超过105, 求其所有子序列的个数。注意相同的只算一次:例如 {1,2,1}有子序列{1} {2} {1,2} {2,1}和{1,2,1}。最后结果对10^9 + 7取余数。

如果没有重复的,答案是dp[i]=dp[i-1]*2(选或不选)
有了重复的dp[i]=dp[i-1]*2-dpj-1

杆子的排列
题目描述:有高为 1, 2, …, n 的 n 根杆子排成一排, 从左向右能看到 L 根, 从右向左能看到 R 根。
求有多少种可能的排列方式。T≤1000;n<=200。

第一瞬间想过dp,但没有尝试去写转移方程。第二瞬间想了组合数学,还尝试着推了公式,结局当然以失败告终。然后就写了30分的部分分,但是写挂了。
观察到n只有200,考虑n^3的算法。如果是dp,就有dp[i][j][k]
显然,我们应该从dp[i][j][k]表示放了i个,从左边能看到j个,从右边能看到k个的方案数。
如果我们考虑从短到长的放杆子,那么杆子可以放最左边,最右边,放中间会露出一截,不好讨论。
所以考虑从长到短的放杆子,一根杆子可以放在最左面,最右面,或者中间。
dp[i][j][k] = dp[i - 1][j - 1][k] + dp[i - 1][j][k - 1] + dp[i - 1][j][k] * (i - 2);
初值:dp[1][1][1]=1;

T2文件排版
题目大意:在一些单词间插入空格,两个单词间若有k个空格,则难看程度为(k-1)^2,读入一段文本,使得难看程度最小。
输出时,每一行的开头和结尾处都必须是一个单词,即每行开头和结尾处不能有空白。
唯一例外的是该行仅有一个单词组成的情况,对于这种情况你可将单词放在该行开头处输出,此时如果该单词比该行应有的长度短则我们指定它的最坏程度为 500,在这种情况下,该行的实际长度即为该单词的长度。1<=N<=80
所有单词的总长度不会超过 10000 个字符,任何一行都不会超过 100 个字符,任何一个单词都
在同一行内。排版后文本行数任意,多余的空格也可删除。

读题发现,题中确定的数据只有N和所有单词长度,所以在构造dp状态时要尽量往上靠。
而题目中说明行数不重要则更加让人肯定了这点。
dp[i][j]表示第i个单词的末尾,在一行的第j个位置的难看程度。
一个单词有两种放法:
1.放在一行的开头位置
(1)独占一行
(2)不独占一行
2.放在一行的中间

所以转移分为两种情况。
1.
(1) d p [ i ] [ n ] = d p [ i − 1 ] [ n ] + 500 ; dp[i][n] = dp[i - 1][n] + 500; dp[i][n]=dp[i1][n]+500;
(2) d p [ i ] [ a [ i ] ] = d p [ i − 1 ] [ n ] ; dp[i][a[i]] = dp[i - 1][n]; dp[i][a[i]]=dp[i1][n];

注意(2)要放在(1)的后面(考虑单词长度为n的情况)。
2.枚举单词i和单词i - 1间的空格数k。

d p [ i ] [ j ] = d p [ i − 1 ] [ j − a [ i ] − k ] + ( k − 1 ) 2 ; dp[i][j] = dp[i - 1][j - a[i] - k] + (k - 1)^2; dp[i][j]=dp[i1][ja[i]k]+(k1)2;

int main(){
	scanf("%d",&n);
	while(~scanf("%s",s)){a[++m] = strlen(s);	}
	memset(dp,inf,sizeof(dp));
	dp[1][n] = 500;dp[1][a[1]] = 0;
	for(int i = 2; i <= m; ++i){
		dp[i][n] = dp[i - 1][n] + 500;
		dp[i][a[i]] = dp[i - 1][n];
		for(int j = a[i] + 1; j <= n; ++j)
			for(int k = 1; a[i] + k < j; ++k)
				dp[i][j] = min(dp[i][j],dp[i - 1][j - a[i] - k] + (k - 1) * (k - 1));
	}
	printf("Minimal badness is %d.\n",min(dp[m][n],dp[m][a[m]] + 500));	
	return 0;
}

8.模后最大
一般情况有两种做法:
1.模数较小,所以可以记录每个状态是否可达。
2.模数较大,同样用set/map记录每个状态是否可达。

51nod 1624 取余最长路
在一个3*n的带权矩阵中,从(1,1)走到(3,n),只能往右往下移动,问在模p意义下的移动过程中的权总和最大是多少。

不难发现路径可以拆成三条线段,只要知道两个转折点的位置就能计算出答案。
设sum(i,l,r)表示第i行从l到r元素的和,则答案可以表示为sum(1,1,x)+sum(2,x,y)+sum(3,y,n)%p。
前缀和一下转化成(S3[n]-S3[y-1])+S2[y]+(S1[x]-S2[x-1])%p,从小到大枚举y,将所有(S1[x]-S2[x-1])扔到一个集合里,用个set.由于A=(S3[n]-S3[y-1])+S2[y]是已知的,只需要lower_bound(st.begin(),st.end(),A)即可。据说st.lower_bound(A)效率更高就不用记录%p意义下的每个数是否可行。
时间复杂度为O(NlogN)。

9.树上的dp

51nod 1603 限高二叉排列树
题意:求有N个节点,高度大于等于H的二叉树种数

直接定义dp[i][j]表示有i个节点,高度大于等于j的树的个数不好转移。所以我们反向思考,定义dp[i][j]表示有i个节点,高度小于j的树的个数。转移时我们选取当前点为根,枚举左右子树
注意如果为空,也要计算,因为肯定小于。

	for(int i=0;i<=n;++i) dp[0][i]=1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)//!!
			for(int k=0;k<i;++k)
				dp[i][j]+=dp[k][j-1]*dp[i-k-1][j-1];	
	printf("%lld\n",dp[n][n]-dp[n][h-1]);

10.DP+栈

POJ 2955 Brackets
题目大意:给出一个括号字符串,问这个字符串中符合规则的最长子串的长度

区间dp,用dp[i][j]表示[i,j]这个区间内符合规则的最长子串的长度
如果str[i] 和str[j]能构成 ()或 [ ],那么dp[i][j] = dp[i + 1][j - 1] + 2
剩下的情况就是dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j])
与下题不同的是下题只有(),所以可以利用栈O(n)。

51nod 1478 括号序列的最长合法子段
如果插入“+”和“1”到一个括号序列,我们能得到一个正确的数学表达式,我们就认为这个括号序列是合法的。例如,序列"(())()", “()“和”(()(()))“是合法的,但是”)(”, "(()“和”(()))("是不合法的。
这里有一个只包含“(”和“)”的字符串,你需要去找到最长的合法括号子段,同时你要找到拥有最长长度的子段串的个数。

我们设dp[i]表示前i个中以i结尾的最长合法括号字段长度。如果当前字符是‘(’,就加入栈中;如果是’)‘就看它匹配的’(’(假如是j),那么以i结尾的括号长度就是dp[j-1]+i-j+1.最后找最长及其个数就好了。

	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;++i){
		if(s[i]=='(') sta[++tp]=i;
		else if(tp){
			dp[i]=dp[sta[tp]-1]+i-sta[tp]+1;
			tp--;
		}
	}
	for(int i=1;i<=n;++i){
		if(dp[i]>ans){
			ans=dp[i];
			sum=1;
		}
		else if(dp[i]==ans) sum++;
	}
	if(ans==0) puts("0 1");
	else printf("%d %d\n",ans,sum);

1791 合法括号子段
有一个括号序列,现在要计算一下它有多少非空子段是合法括号序列。
合法括号序列的定义是:
1.空序列是合法括号序列。
2.如果S是合法括号序列,那么(S)是合法括号序列。
3.如果A和B都是合法括号序列,那么AB是合法括号序列。

与上题同样的思路:设dp[i]表示以i结尾的合法字段数。

不开longlong见祖宗!!

51nod 1464 半回文
一个字符串t是半回文的条件是,对于所有的奇数i(1≤i≤|t|+12),ti = t|t| − i + 1 始终成立,|t|表示字符串t的长度。下标从1开始。例如"abaa", “a”, “bb”, “abbbaa"都是半回文,而"ab”, "bba"和"aaabaa"则不是。
现在有一个字符串s,只由小写字母a,b构成,还有一个数字k。现在要求找出s的半回文子串中字典序排在第k位的串,字符串可以是一样,只要所在的位置不同就是不一样的串。
样例解释:
这个样例中半回文子串是 a, a, a, a, aa, aba, abaa, abba, abbabaa, b, b, b, b, baab,bab, bb, bbab, bbabaab (按照字典序排序).

显然为了求出所有的半回文串,我们用DP预处理。这就涉及到区间dp的O(n^2)写法。然后把所有的半回文子串插入到tried树中,找出第k大即可。又涉及到了trie树找第K大。非常经典。

void insert(int x){
	int u=0;
	for(int i=x;i<=vis[x];++i){
		int d=s[i]-'a';
		if(!t[u][d]) {
			t[u][d]=++idx;
			fa[idx]=u;
		}
		u=t[u][d];
		if(dp[x][i]) cnt[u]++;
	}
}

void bfs(){
	for(int i=idx;i>=1;--i){
		sz[i]+=cnt[i];
		sz[fa[i]]+=sz[i];
	}
	sz[0]=0;
}

void Find(){
	int u=0;
	while(sz[t[u][0]]||sz[t[u][1]]){
		if(m<=cnt[u]) break;//!!
		m-=cnt[u];
		if(sz[t[u][0]]>=m) {
			putchar('a');
			u=t[u][0];
		}
		else {
			putchar('b');
			if(t[u][0]) m-=sz[t[u][0]];
			u=t[u][1];
		}
	}
}

int main(){
//	freopen("a.txt","r",stdin);
	scanf("%s%d",s+1,&m);
	n=strlen(s+1);
	for(int i=n;i>=1;--i){
		dp[i][i]=1;
		vis[i]=i;
		for(int j=i+1;j<=n;++j){
			if(s[i]==s[j]) {
				if(i+2>=j-2) dp[i][j]=1;
				else dp[i][j]=dp[i+2][j-2];
			}
			if(dp[i][j]) vis[i]=j;
		}
	}
	for(int i=1;i<=n;++i)	insert(i);
	bfs();
	Find();
	return 0;
}

51nod 1510 最小化序列
现在有一个长度为n的数组A,另外还有一个整数k。数组下标从1开始。
现在你需要把数组的顺序重新排列一下使得下面这个的式子的值尽可能小。
$∑^{n−k}_{i=1}|A[i]−A[i+k]| $
特别的,你也可以不对数组进行重新排列。

我们发现,就是把数组排序后分成k组,n/k个一组,或者n/k+1个一组。但是具体的分法会影响答案。所以我们用dp来除去后效性。我们定义dp[i][j]表示k+1个一组的数选了i组,k个一组的数选了j组。那么可以通过dp[i-1][j]和dp[i][j-1]转移得到。其中p1表示到dp[i-1][j]用了p1个数,p2表示到dp[i][j-1]用了p2个数。

int main(){
//	freopen("a.txt","r",stdin);
	n=read();k=read();
	for(int i=1;i<=n;++i) a[i]=read();
	sort(a+1,a+n+1);
	
	l1=n/k+1;l2=n/k;
	t1=n%k;t2=k-t1;
	for(int i=1,p=0;i<=t1;++i){
		dp[i][0]=dp[i-1][0]+a[p+l1]-a[p+1];
		p+=l1;
	}
	for(int i=1,p=0;i<=t2;++i){
		dp[0][i]=dp[0][i-1]+a[p+l2]-a[p+1];
		p+=l2;
	}
	for(int i=1;i<=t1;++i){
		for(int j=1,p1,p2;j<=t2;++j){
			p1=(i-1)*l1+j*l2;
			p2=i*l1+(j-1)*l2;
			dp[i][j]=min(dp[i-1][j]+a[p1+l1]-a[p1+1],dp[i][j-1]+a[p2+l2]-a[p2+1]);
		}
	}
	printf("%d\n",dp[t1][t2]);
	return 0;
}

51nod 1522 上下序列
现在有1到n的整数,每一种有两个。要求把他们排在一排,排成一个2*n长度的序列,排列的要求是从左到右看,先是不降,然后是不升。
特别的,也可以只由不降序列,或者不升序列构成.
除了以上的条件以外,还有一些其它的条件,形如"h[xi] signi h[yi]",这儿h[t]表示第t个位置的数字,signi是下列符号之一:’=’ (相等), ‘<’ (小于), ‘>’ (大于), ‘<=’ (小于等于), ‘>=’ (大于等于)。这样的条件有k个。
请计算一下有多少种序列满足条件。

这道题主要的思路是区间DP。每次把两个相同的数字填进已有的序列[l, r]中。

https://www.cnblogs.com/RabbitHu/p/51nod1522.html
但是由于题目中有这些限制,显然不能瞎填对吧……那么怎么判断能不能这样填呢?
方便起见,从大到小、从中间到两边填(因为原题要求中间比两边大)。
既然后填的、两边的数一定比先填的、中间的数小,那么只需判断当前要填的位置是否有“大于、大于等于、或等于已经填的区间内的元素”的限制就好了。另外,当前填的两个元素中,不能有“一个大于另一个”的限制。
具体来说,如果用dp[i][j]表示填满区间[i, j]的方案数,check(a, b, l, r) == 1 表示a、b不会“大于、大于等于、或等于[l, r]内的元素”且没有"a>b“或”b>a"的限制。
那么:
若都填到左边:if(check(i, i + 1, i + 2, j)) dp[i][j] += dp[i + 2][j]
若都填到右边:if(check(j - 1, j, i, j - 2)) dp[i][j] += dp[i][j - 2]
若两边各填一个: if(check(i, j, i + 1, j - 1)) dp[i][j] += dp[i + 1][j - 1]

不开longlong见祖宗!!

bool check(int a,int b,int l,int r){
	for(int i=1;i<=nlar[a];++i)
		if((l<=lar[a][i]&&lar[a][i]<=r)||lar[a][i]==b) 
			return false;
	for(int i=1;i<=neq[a];++i)
		if(l<=eq[a][i]&&eq[a][i]<=r) 
			return false;
	for(int i=1;i<=nleq[a];++i)
		if(l<=leq[a][i]&&leq[a][i]<=r) 
			return false;
	return true;
}

int main(){
	n=read();k=read();
	for(int i=1,a,b;i<=k;++i){
		s[0]=s[1]=0;
		a=read();scanf("%s",s);b=read();
		if(s[0]=='=') {
			eq[a][++neq[a]]=b;
			eq[b][++neq[b]]=a;
		}
		else{
			if(s[0]=='<') swap(a,b);
			if(s[1]=='=') leq[a][++nleq[a]]=b;
			else lar[a][++nlar[a]]=b;
		}
	}
	
	for(int i=1;i<2*n;++i)
		if(check(i,i+1,0,0)&&check(i+1,i,0,0)) dp[i][i+1]=1;
	
	for(int len=2;len<=2*n;len+=2)
		for(int i=1;i+len-1<=2*n;++i){
			int j=i+len-1;
			if(check(i,i+1,i+2,j)&&check(i+1,i,i+2,j))
				dp[i][j]+=dp[i+2][j];
			if(check(j-1,j,i,j-2)&&check(j,j-1,i,j-2))
				dp[i][j]+=dp[i][j-2];
			if(check(i,j,i+1,j-1)&&check(j,i,i+1,j-1))
				dp[i][j]+=dp[i+1][j-1];
		}
	
	printf("%lld\n",dp[1][n<<1]);
	
	return 0;
}

51nod 1218 最长递增子序列 V2
给出一个序列,求可能出现在LIS中的数的下标,一定会出现在LIS中的数的下标

一个数从头到它的LIS长度+从它到尾的LIS长度如果等于LIS+1,那么这个数可能出现在LIS中。
当一个数一定存在时,假设从头到它的LIS长度为x,那么所有f(x+1)必然由它转移得到。也就是说,从头到一个数本身的LIS长度为f(x)的只有这个数。

代码实现过程中注意:求从它到为的LIS长度等价于求从尾到它的LDS长度。由于lower_bound必须用于一个单调不递减的数组,所以我们仍然是求LIS,但是是求 − a i -a_i ai的。

	n=read();
	for(int i=1;i<=n;++i)
		a[i]=read();
	
	b[0]=-INF;
	for(int i=1;i<=n;++i){
		if(a[i]>b[cnt]) {
			b[++cnt]=a[i];lis[i]=cnt;
		}
		else{
			int pos=lower_bound(b+1,b+cnt+1,a[i])-b;
			b[pos]=a[i];
			lis[i]=pos;
		}
	}
	Len=cnt;
	cnt=0;
	b[0]=-INF;
	for(int i=n;i>=1;--i){
		a[i]=-a[i];
		if(a[i]>b[cnt]){
			b[++cnt]=a[i];lds[i]=cnt;
		}
		else{
			int pos=lower_bound(b+1,b+cnt+1,a[i])-b;
			b[pos]=a[i];
			lds[i]=pos;
		}
	}
	
	for(int i=1;i<=n;++i){
		if(lis[i]+lds[i]==Len+1){
			ty[i]=1;
			c[lis[i]]++;
		}
	}
	
	printf("A:");
	for(int i=1;i<=n;++i)
		if(ty[i]&&c[lis[i]]!=1)
			printf("%d ",i);
	putchar('\n');
	printf("B:");
	for(int i=1;i<=n;++i)
		if(ty[i]&&c[lis[i]]==1)
			printf("%d ",i);
	putchar('\n');
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值