非对称DP

目录

一,对称DP、非对称DP

1,对称DP

2,非对称DP

二,非对称DP的OJ实战

CSU 1207: Strictly-increasing sequence

HDU 1176 免费馅饼

POJ - 1390 Blocks

POJ 3661 Running


一,对称DP、非对称DP

这是我根据自己对DP的理解和归纳,提出来的新概念。

1,对称DP

数列DP https://blog.csdn.net/nameofcsdn/article/details/112771904

区间DP https://blog.csdn.net/nameofcsdn/article/details/112981922

这2类DP其实都比较简单,因为对象空间等于解空间 https://blog.csdn.net/nameofcsdn/article/details/111885131

如果问题要求的是f(i,j),那么算法中的递推式就会是f(i,j)=......

因为这里的i和j是同一个概念,地位对称,也因为对象空间和解空间的地位对称,所以我称之为对称DP

2,非对称DP

有一些DP问题,比如问题抽象成f(n),f的递推式很复杂甚至有可能根本不存在,

但是我们可以引入一个辅助概念,在对应的辅助空间内取一个变量i,得到子问题g(n,i)

根据g(n,i)的递推式算出其值,最后根据f(n)=h({g(n,i)|i∈A})算出f(n)

其中h表示的是如何根据子问题的解整合得到原问题的解,一般就是sum函数或者max函数或者min函数,而A表示的就是辅助空间

因为n所在的对象空间和辅助空间A共同构成解空间,即对象空间和解空间不同,也因为n和i不是同一个概念,所以我称之为非对称DP

二,非对称DP的OJ实战

CSU 1207: Strictly-increasing sequence

题目:

Description

如果一个序列中任意一项都大于前一项,那么我们就称这个序列为严格递增序列。

现在有一个整数序列,你可以将序列中任意相邻的若干项合并成一项,合并之后这项的值为合并前各项的值之和。通过若干次合并,最终一定能得到一个严格递增序列,那么得到的严格递增序列最多能有多少项呢?

Input

输入数据的第一行包含正整数T (1 <= T <= 200),表示接下来一共有T组测试数据。

每组测试数据的第一行包含一个整数N (1 <= N <= 1000),表示这个整数序列一共有N项。接下来一行包含N个不大于10^6的正整数,依次描述了这个序列中各项的值。

至多有20组数据满足N > 100。

Output

对于每组测试数据,用一行输出一个整数,表示最终得到的严格递增序列最多能有多少项。

Sample Input

3
2
1 1
3
1 2 3
5
1 3 2 6 7

Sample Output

1
3
4

思路:区间DP

dp[i][j]表示从1到j这j个数,最后一段为i到j的情况下,最优解的值(可能为0)

代码:

#include<iostream>
using namespace std;
 
int dp[1001][1001];//dp[i][j]表示最后一段为i到j的最优解
 
int main()
{
	int T, n, num[1001], sum[1001];
	cin >> T;
	while (T--)
	{
		cin >> n;
		sum[0] = 0;
		for (int i = 1; i <= n; i++)
		{
			cin >> num[i];
			dp[1][i] = 1, dp[i][i - 1] = 0;
			sum[i] = sum[i - 1] + num[i];
		}
		for (int i = 1; i < n; i++)
		{
			int ki = i;
			for (int j = i + 1; j <= n; j++)
			{
				dp[i + 1][j] = dp[i + 1][j - 1];
				while (ki)
				{
					if (sum[i] - sum[ki-1] >= sum[j] - sum[i])break;
					if (dp[ki][i] > 0 && dp[i + 1][j] < dp[ki][i] + 1)
						dp[i + 1][j] = dp[ki][i] + 1;
					ki--;
				}
			}
		}
		int ans = 0;
		for (int i = 1; i <= n; i++)if (ans < dp[i][n])ans = dp[i][n];
		cout << ans << endl;
	}
	return 0;
}

HDU 1176 免费馅饼

题目:

Description

都说天上不会掉馅饼,但有一天gameboy正走在回家的小径上,忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了,这馅饼别处都不掉,就掉落在他身旁的10米范围内。馅饼如果掉在了地上当然就不能吃了,所以gameboy马上卸下身上的背包去接。但由于小径两侧都不能站人,所以他只能在小径上接。由于gameboy平时老呆在房间里玩游戏,虽然在游戏中是个身手敏捷的高手,但在现实中运动神经特别迟钝,每秒种只有在移动不超过一米的范围内接住坠落的馅饼。现在给这条小径如图标上坐标: 
为了使问题简化,假设在接下来的一段时间里,馅饼都掉落在0-10这11个位置。开始时gameboy站在5这个位置,因此在第一秒,他只能接到4,5,6这三个位置中其中一个位置上的馅饼。问gameboy最多可能接到多少个馅饼?(假设他的背包可以容纳无穷多个馅饼) 

Input

输入数据有多组。每组数据的第一行为以正整数n(0<n<100000),表示有n个馅饼掉在这条小径上。在结下来的n行中,每行有两个整数x,T(0<T<100000),表示在第T秒有一个馅饼掉在x点上。同一秒钟在同一点上可能掉下多个馅饼。n=0时输入结束。 

Output

每一组输入数据对应一行输出。输出一个整数m,表示gameboy最多可能接到m个馅饼。 
提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。 

Sample Input

6
5 1
4 1
6 1
7 2
7 2
8 3
0

Sample Output

4

这个题目很明显就是动态规划了,可以理解为11个数组的DP问题,相当于数学归纳法里面的螺旋归纳法的加强版。

代码:

#include<iostream>
#include<stdio.h>
using namespace std;
 
 
int n, x, t;
int num[100001][11];
 
int get(int i, int j)
{
	int r = num[i - 1][j];
	if (j && r < num[i - 1][j - 1])r = num[i - 1][j - 1];
	if (j < 10 && r < num[i - 1][j + 1])r = num[i - 1][j + 1];
	return r;
}
 
int main()
{
	while (scanf("%d", &n))
	{
		if (n == 0)break;
		for (int j = 0; j <= 100000; j++)
			for (int i = 0; i < 11; i++)num[j][i] = 0;
		for (int i = 1; i <= n; i++)
		{
			scanf("%d%d", &x, &t);
			if (x >= 5 - t && x <= 5 + t)num[t][x] ++;
		}
		for (int j = 1; j <= 100000; j++)
			for (int i = 0; i < 11; i++)num[j][i] += get(j, i);		
		int maxx = 0;
		for (int i = 0; i < 11; i++)
			if (maxx < num[100000][i])maxx = num[100000][i];
		printf("%d\n", maxx);
	}
	return 0;
}

不管输入的是什么样的,程序是不需要排序的。

这个代码还可以继续优化时间,好多地方不需要用到100000,看输入的数据决定需要弄多大。

不过这个也就92ms就AC了,懒得再改了。

有一个地方需要注意,如果输入的n个馅饼中,其中1个是3,1,那这个馅饼是绝对接不到的。

这种情况需要区分开来,处理方法就是直接忽略不计数就好了。

判定条件是x >= 5 - t && x <= 5 + t,满足这个的都是可以接到的馅饼,不满足的都是接不到的。

POJ - 1390 Blocks

题目:

Some of you may have played a game called 'Blocks'. There are n blocks in a row, each box has a color. Here is an example: Gold, Silver, Silver, Silver, Silver, Bronze, Bronze, Bronze, Gold. 
The corresponding picture will be as shown below: 
If some adjacent boxes are all of the same color, and both the box to its left(if it exists) and its right(if it exists) are of some other color, we call it a 'box segment'. There are 4 box segments. That is: gold, silver, bronze, gold. There are 1, 4, 3, 1 box(es) in the segments respectively. 
Every time, you can click a box, then the whole segment containing that box DISAPPEARS. If that segment is composed of k boxes, you will get k*k points. for example, if you click on a silver box, the silver segment disappears, you got 4*4=16 points. 
Now let's look at the picture below: 
The first one is OPTIMAL. 
Find the highest score you can get, given an initial state of this game. 

Input

The first line contains the number of tests t(1<=t<=15). Each case contains two lines. The first line contains an integer n(1<=n<=200), the number of boxes. The second line contains n integers, representing the colors of each box. The integers are in the range 1~n.

Output

For each test case, print the case number and the highest possible score.

Sample Input

2
9
1 2 2 2 2 3 3 3 1
1
1

Sample Output

Case 1: 29
Case 2: 1

代码:

#include<iostream>
using namespace std;
 
int num[201], col[201], ans[201][201][201];//前面附加t个同色盒子
 
int dp(int l, int r,int t)
{
	if (l > r)return 0;
	if (l == r)return (num[l] + t) * (num[l] + t);
	if (ans[l][r][t])return ans[l][r][t];
	int a = dp(l, l, t) + dp(l + 1, r, 0);
	for (int i = l + 1; i <= r; i++)
	{
		if (col[i] != col[l])continue;
		if (a < dp(l + 1, i - 1, 0) + dp(i, r, t + num[l]))
			a = dp(l + 1, i - 1, 0) + dp(i, r, t + num[l]);
	}
	ans[l][r][t] = a;
	return a;
}
int main()
{
	int T, n;
	cin >> T;
	for(int i=1;i<=T;i++)
	{
		cin >> n;
		int c1 = -1, c2, key = 0;
		for (int i = 0; i <= n; i++)
		{
			num[i] = 0;
			for (int j = 0; j <= n; j++)for(int t=0;t<=n;t++)ans[i][j][t] = 0;
		}
		for (int i = 0; i < n; i++)
		{
			cin >> c2;
			if (c1 != c2)c1 = c2, col[++key] = c2;
			num[key]++;
		}
		cout << "Case " << i << ": " << dp(1, key, 0) << endl;
	}
	return 0;
}

POJ 3661 Running

题目:

Description

The cows are trying to become better athletes, so Bessie is running on a track for exactly N (1 ≤ N ≤ 10,000) minutes. During each minute, she can choose to either run or rest for the whole minute.

The ultimate distance Bessie runs, though, depends on her 'exhaustion factor', which starts at 0. When she chooses to run in minute i, she will run exactly a distance of Di (1 ≤ Di ≤ 1,000) and her exhaustion factor will increase by 1 -- but must never be allowed to exceed M (1 ≤ M ≤ 500). If she chooses to rest, her exhaustion factor will decrease by 1 for each minute she rests. She cannot commence running again until her exhaustion factor reaches 0. At that point, she can choose to run or rest.

At the end of the N minute workout, Bessie's exaustion factor must be exactly 0, or she will not have enough energy left for the rest of the day.

Find the maximal distance Bessie can run.

Input

* Line 1: Two space-separated integers: N and M
* Lines 2..N+1: Line i+1 contains the single integer: Di

Output

* Line 1: A single integer representing the largest distance Bessie can run while satisfying the conditions.

Sample Input

5 2
5
3
4
2
10

Sample Output

9

这个题目,我从头到尾产生过4种思路,前面3种都挂了,最后一种终于成功了。

思路一,超内存+超时,不能AC但是代码是对的,只不过效率很低。

思路二,无法满足题目的限制条件,代码是错的。

思路三,超时,代码是对的,但是效率还不够高。

思路四,完全正确。

思路一:把时间分成很多段,每一段都从exaustion factor为0开始,又以exaustion factor为0结束,求最多能跑多远。

sum[i][j]表示的就是从i到j的时间内,最多能跑多远,要满足上述条件。

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][10001];
int a;
int n, m;
 
int f(int i, int j)		
{
	if (sum[i][j] >= 0)return sum[i][j];
	if (i >= j)return 0;
	if (i + 1 == j)return list[i];
	if (j - i + 1 <= m * 2 && (j - i) % 2)
	{
		sum[i][j] = 0;
		for (int k = i; k <= (i + j) / 2; k++)sum[i][j] += list[k];
	}
	for (int k = i; k < j; k++)
	{
		a = f(i, k) + f(k + 1, j);
		if (sum[i][j] < a)sum[i][j] = a;
	}
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(1, n);
	return 0;
}

后来提交了才发现,1万乘1万的数组太大了,超内存了。

其实不仅仅是内存的问题,这个思路的时间复杂度很高,不可取。

思路二:求到第 i 分钟结束,如果exaustion factor为 j 的话最多可以跑多远。

sum[i][j]表示的是对于任意i不超过n,任意j不超过m,第 i 分钟结束疲劳系数为j的情况下最多可以跑多远。

为什么我要强调任意,有深意,请往下看(在思路三的第4行)

只有对自己定义的状态sum数组十分的清楚,没有一点含糊,才不容易出错。

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][501];
int a,b;
int n, m;
 
int f(int i, int j)		
{
	if (sum[i][j] >= 0)return sum[i][j];
	if (i == 1)
	{
		if (j)return list[1];
		return 0;
	}
	if (j > 0)sum[i][j] = f(i - 1, j - 1) + list[i];
	if (j < m)
	{
		a = f(i - 1, j + 1);
		if (sum[i][j] < a)sum[i][j] = a;
		if (j == 0)
		{
			a = f(i - 1, 0);
			if (sum[i][j] < a)sum[i][j] = a;
		}
	}
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(n, 0);
	return 0;
}

这里有个细节我不是很确定,就是sum[1][j]里面,如果j大于1到底有没有意义。

我觉得虽然sum[1][j]里面的j大于1是不现实的,但是应该对我们求解本问题的最优解是没有影响的,不过我不是很确定。

这个代码没有保证题目的限制条件:一旦开始休息,必须要休息到exaustion factor为0才能继续跑,果断放弃。

思路三:sum[i][j]表示的是到第 i 分钟结束,如果exaustion factor为 j 的话最多可以跑多远,

但是有个限制条件,如果 j 不是0的话,它表示的是exaustion factor关于时间的函数的极大值点。

也就是说,f(6,3)表示在第4、5、6分钟都在跑,而且跑完之后exaustion factor为0的情况下,最多可以跑多远。

这样的状态就很有意思了,不是对于跑的过程中可能产生的任意状态都定义sum,

而只对exaustion factor关于时间变化的函数的极大值处和零点处才定义sum。

请注意,不是不计算某些sum[i][j],而是不定义!前者是计算方法的略微区别,后者是思想的本质区别!

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][501];
int a;
int n, m;
 
int f(int i, int j)
{
	if (i <= 0)return 0;
	if (sum[i][j] >= 0)return sum[i][j];	
	if (j == 0)
	{
		sum[i][j] = f(i - 1, 0);		//第i分钟在休息
		for (int k = i-m; k < i; k++)
		{
			a = f(k, i - k);
			//第k分钟在跑,到了顶点,然后开始休息,直到第i分钟结束才让疲劳度降为0
			if (sum[i][j] < a)sum[i][j] = a;
		}
	}
	else
	{
		a = 0;		//j不为0那么就是疲劳度的极大值点
		int ki = i;	//不需要进行任何的分类讨论,也不需要max函数之类的
		for (int kj = j; ki>0 && kj>0; ki--,kj--)a += list[ki];
		sum[i][j] = a + f(ki, 0);
	}
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(n, 0);
	return 0;
}

这个代码,还是超时了。

仔细一想,主要是f函数里面,当j不为0时有一个循环。

虽然不需要任何讨论,那个循环看起来也很短,但是在f不停的调用的情况下,这个循环的工作是很大的重复了的。

比如说,跑步方案一,1-20分钟在跑步,21-40分钟休息,

跑步方案二,1-19分钟在跑步,20-40分钟在休息。

显然方案一优于方案二,我们人在思考这个对比的时候其实是发现了这2个方案具有很高的相似性的。

对于比较任意的2个方案,我们就无法产生这种感觉了。

看上面的代码,计算sum[20][20]和计算sum[19][19],我们发现了什么?

天哪!几乎所有的工作(即加法运算)都是重复的!

意识到了问题的关键所在,我们离答案就不远了。

思路四:sum[i][j]表示的是到第 i 分钟结束,如果exaustion factor为 j 的话最多可以跑多远,

但是有个限制条件,如果 j 不是0的话,它表示的是第i分钟在跑的情况下最多可以跑多远。

对比思路三,我们可以发现,思路三只定义疲劳度函数的极大值点和零点处的sum,非常稀疏,

而思路四定义那些j为0或者第i分钟在跑的那些sum。

(顺便一提,这样定义的sum实际上是刚好定义了一半的,因为跑1分钟疲劳度加1,休息1分钟疲劳度减1

疲劳度函数由很多左右对称的凸峰组成。虽然凸峰之间的距离可以很大,但是在动态规划的算法中,

因为要求最优解,实际上凸峰之间都是距离不超过1的,不然的话中间又可以跑1分钟休息1分钟,形成1个小峰)

当然,这不是什么重要的发现,重要的是,思路三里面提到的重复计算,在思路四里面可以解决了!

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[10001];
int sum[10001][501];
int a;
int n, m;
 
int f(int i, int j)
{
	if (i <= 0)return 0;
	if (sum[i][j] >= 0)return sum[i][j];	
	if (j == 0)
	{
		sum[i][j] = f(i - 1, 0);
		for (int k = i-m; k < i; k++)
		{
			a = f(k, i - k);
			if (sum[i][j] < a)sum[i][j] = a;
		}
	}
	else sum[i][j] = f(i - 1, j - 1) + list[i];
	return sum[i][j];
}
 
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> list[i];
	memset(sum, -1, sizeof(sum));
	cout << f(n, 0);
	return 0;
}

思路四的代码,和思路三几乎差不多,唯一的区别就是,f函数里面,当j不为0的时候,实际上只要一条简单的语句就可以算出sum[i][j]

其他的地方都不需要修改。

如果仔细推敲的话,我们可以发现,思路三和思路四都能处理思路二的漏洞——满足题目的限制,休息的时候必须休息到疲劳度为0才能继续跑。

但是方式是有不小的区别的。

思路三是在求顶点(j不为0)处的sum的时候,直接用求和式往前推到开始跑(疲劳度为0)的时候。

而思路四却是在求j为0处的sum的时候,直接往前推到开始休息的时候。

虽然2个思路的代码在求j为0处的sum的时候是一模一样的,但是因为思路不同,思想不同,所以选择用来满足该限制的方式不同,才导致效率不一样。

差不多是我能AC的最难DP问题了。

猜你喜欢

转载自blog.csdn.net/nameofcsdn/article/details/113064148