WHUT第七周训练整理

WHUT第七周训练整理

祝各位武理的ACMer们,新年快乐!在接下来的一年里继续训练,为武理的ACM揭开新的篇章!同为武汉的学子,武汉加油!

索引

(难度由题目自身难度与本周做题情况进行分类,仅供新生参考!)

零、动态规划与贪心算法的区别

一、easy:01、02、03、06、11、15

二、medium:04、05、07、08、09、10、12、13、14、16、17、18、20

三、hard:19、21、22

本题解报告大部分使用的是C++语言,在必要的地方使用C语言解释。

零、动态规划与贪心算法的区别

贪心算法(Greedy algorithm): 从问题解题过程一步一步按照某个策略取当前的最优值,并且达到全局最优结果;
动态规划(DP):在解决当前问题的时候考虑到之前所有的结果使得过去最优。因此动态规划得到的是每时每刻的全局最优解,并且逐步扩大问题规模。动态规划的精髓是组合子问题。

从方法上来说,
贪心算法需要严格证明全局正确性,并合理设计当前策略;
动态规划算法需要设计状态转移方程,考虑到状态转移的所有情况。

从效率上来说,贪心算法在一般情况下快于动态规划——当然前提是两种算法都能得到正确的结果。

作者:知乎用户

扫描二维码关注公众号,回复: 9254986 查看本文章

链接:https://www.zhihu.com/question/36662980/answer/68494301

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一、easy

1001:Bone Collector(01背包)

题意:现在有 N N​ 根骨头还有一个容量为 V V​ 的背包,每根骨头都有自身的重量以及价值,问怎么选择骨头放入背包在能获得最大价值呢?

范围: N 1000   , V 1000 N \le 1000~,V \le 1000​

分析:经典的背包背景,也是 d p dp 的入门题,即 01 01 背包,每个物品只有取和不取两种状态,对应 0 0 1 1

接下来我们来考虑怎么设计,首先影响答案的因素有哪些?

  1. 可以选择的物品
  2. 剩余背包的容量

那么设 d p [ i ] [ j ] dp[i][j] 表示可以在 1 i 1\sim i 物品中进行选择,且可用容量为 j j 时能够取到的最大价值。

转移方程:

  1. 如果当前的可用容量 >= 物品i的重量,那么 d p [ i ] [ j ] = m a x ( d p [ i 1 ] [ j ] , d p [ i 1 ] [ j w e i g h t [ i ] ] + v a l u e [ i ] ) ; dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]);

    d p [ i ] [ j 1 ] dp[i][j-1] 表示不选择物品 i i ,在 1 i 1 1 \sim i-1 物品中进行选择的最优解; v a l u e [ i ] + d p [ i ] [ j w e i g h t [ i ] ] value[i]+dp[i][j-weight[i]] 表示选择物品 i i ,还有剩余的容量在 1 i 1 1\sim i-1 物品中进行选择的最优解)

  2. 如果当前的可用容量 < 物品 i i 的重量,那么 d p [ i ] [ j ] = d p [ i 1 ] [ j ] ; dp[i][j] = dp[i-1][j];​

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000 + 10;

int n, v;
int dp[MAXN][MAXN];
int value[MAXN], weight[MAXN];  // 物品的价值与重量

int main()
{
	int T;
	cin >> T;
	while (T--)
	{
		memset(dp, 0, sizeof(dp));  // 多组输入注意清空数组
		cin >> n >> v;
		for (int i = 1; i <= n; i++)
		{
			cin >> value[i];
		}
		for (int i = 1; i <= n; i++)
		{
			cin >> weight[i];
		}
        // 两种转移方程
		for (int i = 1; i <= n; i++)
		{
			for (int j = 0; j <= v; j++)
			{
				if (j < weight[i])
					dp[i][j] = dp[i - 1][j];
				else
					dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
			}
		}
		cout << dp[n][v] << endl;
	}
	return 0;
}

1002:Super Jumping! Jumping! Jumping!(dp)

题意:给 N N​ 个数字 a r r [ 1 ] a r r [ n ] arr[1] \sim arr[n]​ ,可以从任意位置开始从左往右跳,但是只能跳到数字比当前数字大的位置,到达位置上的数字都可以收集起来,问最大能够收集多少呢?

范围: N 1000   ,   a r r [ i ] 2 32 N \le 1000~,~arr[i] \le 2^{32}

分析:没想到可以证明正确性的贪心策略,那么考虑 d p dp​

影响当前状态的因素有哪些?

  1. 选择区间
  2. 终点位置

d p [ i ] dp[i] 表示可以在区间 [ 1 , i ] [1, i] 中进行选择并且以第 i i 个数字为终点的最优解。因为起点是任意的,所以 d p [ i ] dp[i] 的初值就是自身 a r r [ i ] arr[i]​

转移方程:

d p [ i ] = m a x ( d p [ i ] , d p [ j ] + a r r [ i ] ) dp[i] = max(dp[i], dp[j]+arr[i]) j < i j < i a r r [ j ] < a r r [ i ] arr[j] < arr[i]

(满足路径上的数字是严格递增的)

时间复杂度是 O ( n 2 ) O(n^2) ,可以接受。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000 + 10;

int n;

int arr[MAXN], dp[MAXN];

int main()
{
	while (cin >> n, n)
	{
		memset(dp, 0, sizeof(dp));  // 多组输入注意清空数组
		for (int i = 1; i <= n; i++)
		{
			cin >> arr[i];
		}
		int ans = 0;
		for (int i = 1; i <= n; i++)
		{
			dp[i] = arr[i];  // 初始值就是自身
            // 寻找前面数字比自己小的地方进行比较更新
			for (int j = 1; j < i; j++)
			{
				if (arr[j] < arr[i])
					dp[i] = max(dp[i], dp[j] + arr[i]);
			}
			ans = max(ans, dp[i]);  // 最优解的终点在1~n之间,故取最大值
		}
		cout << ans << endl;
	}
	return 0;
}

1003:命运(dp)

题意:给一个 N M N*M​ 的二维矩阵,现在位于 ( 0 , 0 ) (0,0)​ ,要到 ( N , M ) (N, M)​ 。若当前位置为 ( x , y ) (x,y)​ ,那么可以移动到 ( x + 1 , y ) ( x , y + 1 ) ( x , y k ) (x+1, y)、(x, y+1)、(x,y*k)​ 这些位置,其中 k > 1 k > 1​ ,并且将到达位置上的数字 a r r [ i ] [ j ] arr[i][j]​ (可正可负)收集起来,问到达 ( N , M ) (N, M)​ 时收集的最大值是多少?

范围: 1 N 20   ,   1 M 1000   ,   a r r [ i ] [ j ] < 100 1 \le N \le 20~, ~1 \le M \le 1000~,~|arr[i][j]| < 100

分析:没想到可以证明正确性的贪心算法,考虑使用 d p dp​

此类问题的状态转移还是比较容易想到的,考虑点 ( n , m ) (n, m)​ ,只能是从 ( n 1 , m ) ( n , m 1 ) ( n , k ) (n-1,m)、(n, m-1)、(n, k)​ 这些点转移来, k k​ 满足 m % k = = 0 m\%k==0​

影响因素有哪些?

  1. 可供使用的行区间
  2. 可供使用的列区间

d p [ i ] [ j ] dp[i][j]​ 表示到达位置 ( i , j ) (i, j)​ 时的最优解。

转移方程:

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i 1 ] [ j ] + a r r [ i ] [ j ] ) dp[i][j] = max(dp[i][j], dp[i-1][j]+arr[i][j]) i > 1 i > 1

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i ] [ j 1 ] + a r r [ i ] [ j ] ) dp[i][j] = max(dp[i][j], dp[i][j-1]+arr[i][j]) j > 1 j > 1

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i ] [ k ] + a r r [ i ] [ j ] ) dp[i][j] = max(dp[i][j], dp[i][k]+arr[i][j]) j % k = = 0 j\%k == 0

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 20 + 10;
const int MAXM = 1000 + 10;

int n, m;
int arr[MAXN][MAXM], dp[MAXN][MAXM];

int main()
{
	int T;
	cin >> T;
	while (T--)
	{
		cin >> n >> m;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= m; j++)
			{
				cin >> arr[i][j];
				dp[i][j] = -200;  // 这里-200代表的是极小值
			}
		}
		dp[1][1] = arr[1][1];  // 第一个点不能通过转移得到,所以先预处理
        // 根据三个转移方程进行计算
		for (int i = 1; i <= n; i++)
		{
            
			for (int j = 1; j <= m; j++)
			{
				if (i == 1 && j == 1)
					continue;
				if (i - 1 >= 1)
					dp[i][j] = max(dp[i][j], dp[i - 1][j]);
				if (j - 1 >= 1)
					dp[i][j] = max(dp[i][j], dp[i][j - 1]);
				for (int k = 2; k <= j; k++)  // 注意 k > 1
				{
					if (j % k == 0)
					{
						dp[i][j] = max(dp[i][j], dp[i][j / k]);
					}
				}
				dp[i][j] += arr[i][j];
			}
		}
		cout << dp[n][m] << endl;
	}
	return 0;
}

1006:数塔(dp)

题意:给一个数塔,每个格子都有数字 a r r [ i ] [ j ] arr[i][j]​ ,现在要求从顶层到达底层能够获得的最大数字之和。

范围: 1 N 100   ,   0 a r r [ i ] [ j ] < 100 1 \le N \le 100~, ~0 \le arr[i][j] < 100 ​

分析:没想到可以证明正确性的贪心算法,考虑使用 d p dp

影响因素有哪些?

  1. 到达左上方的最大数字之和
  2. 到达右上方的最大数字之和

d p [ i ] [ j ] dp[i][j]​ 表示到达位置 ( i , j ) (i, j)​ 时的最大数字之和,那么答案就是 d p [ n ] [ j ] dp[n][j]​ 中的最大值。

转移方程:

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] d p [ i 1 ] [ j ] ) + a r r [ i ] [ j ] dp[i][j] = max(dp[i][j], dp[i-1][j])+arr[i][j] i 1 > = 1 i-1 >= 1

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i 1 ] [ j 1 ] ) + a r r [ i ] [ j ] dp[i][j] = max(dp[i][j], dp[i-1][j-1])+arr[i][j] i 1 > = 1 i-1 >= 1 j 1 > = 1 j-1 >= 1

(需要保证 ( i 1 , j ) (i-1, j) ( i 1 , j 1 ) (i-1, j-1)​ 这两个点是存在的)

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;

int n;

int arr[MAXN][MAXN], dp[MAXN][MAXN];

int main()
{
	int T;
	cin >> T;
	while (T--)
	{
        // 初始化
		memset(arr, -1, sizeof(arr));  
		memset(dp, 0, sizeof(dp));
		cin >> n;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= i; j++)
			{
				cin >> arr[i][j];
			}
		}
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= i; j++)
			{
                // 右上方数字存在
				if (arr[i - 1][j] != -1)
					dp[i][j] = max(dp[i][j], dp[i - 1][j]);
                // 左上方数字存在
				if (arr[i - 1][j - 1] != -1)
					dp[i][j] = max(dp[i][j], dp[i - 1][j - 1]);
				dp[i][j] += arr[i][j];  // 加上当前数字
			}
		}
        // 取最后一层的最大值
		int ans = 0;
		for (int i = 1; i <= n; i++)
		{
			ans = max(ans, dp[n][i]);
		}
		cout << ans << endl;
	}
	return 0;
}

1011:最少拦截系统(dp/贪心)

不说了,之前第四周的贪心场训练已经做过了,可以参考我第四周的题解报告 1007 1007​

https://blog.csdn.net/qq_41765114/article/details/103386011

1015:Doing Homework again(贪心/dp)

同样第四周贪心场已经做过了,可以去看看。

https://blog.csdn.net/qq_41765114/article/details/103386011

二、medium

1004:Monkey and Banana(拆点+dp)

题意:给 N N​ 种长方体,每种长方体数量不限,现在要将长方体垒起来,要保证上面长方体的底面面积严格小于下面长方体的顶面面积。现在问最高能够垒到多高?

范围: N 30 N \le 30

分析:因为需要同时考虑一个面的长度和宽度,不好贪心,因此考虑使用 d p dp​

首先对于一个长方体 x y z x*y*z ,其底面的尺寸有三种情况: x y y z x*y、y*z 以及 x z x*z ,所以把一个长方体拆分成三个面来考虑。

我们可以考虑在第 i i 层的时候选择 x y x*y​ 的面,那么下面紧贴着这个面的选择可能有很多种,我们只要求出那些可以选择的面中的最优解就可以了,而且这些底下的面可以事先以同样的方式求得。

影响因素有哪些?

  1. 当前层数
  2. 当前层使用的是第几个面

d p [ i ] [ j ] dp[i][j] 表示第 i i 层选择第 j j 个面作为顶层的最优解。

但是最多会有多少层呢?可以推算出任意一个块自己叠在自己上面,至多只能叠加一次。那么我们可以知道在 n n​ 最大为 30 30​ 的情况,最多有 90 90​ 个面,每个面最多叠加一次,因此最多有 180 180​ 层。

转移方程:

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i 1 ] [ k ] + h e i g h t ) ; dp[i][j] = max(dp[i][j], dp[i - 1][k] + height);​

这里 k k​ 是满足条件的面序号, h e i g h t height​ 是当前长方体的高度

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 30 + 10;

int n;

int arr[MAXN * 3][3], dp[MAXN * 6][MAXN * 3];  // 有3n个面,有6n层

int main()
{
	int kase = 1;
	while (cin >> n, n)
	{
		memset(dp, -1, sizeof(dp));  // 初始化
		int index = 1;
		for (int i = 0; i < n; i++)
		{
			int l, w, h;
			cin >> l >> w >> h;
            // 分别保存三个面,[2]中保存高度
			arr[index][0] = l, arr[index][1] = w, arr[index++][2] = h;
			arr[index][0] = l, arr[index][1] = h, arr[index++][2] = w;
			arr[index][0] = w, arr[index][1] = h, arr[index++][2] = l;
		}
        // 第一层预处理,高度可以直接确定
		for (int i = 1; i < index; i++)
		{
			dp[1][i] = arr[i][2];
		}
        // 其他层按照转移方程计算
		for (int i = 2; i <= 200; i++)
		{
			for (int j = 1; j < index; j++)
			{
				for (int k = 1; k < index; k++)
				{
					if (dp[i - 1][k] == -1)
						continue;
					int w1 = arr[j][0], w2 = arr[k][0], l1 = arr[j][1], l2 = arr[k][1];
                    // 满足条件
					if ((w1 < w2 && l1 < l2) || (w1 < l2 && l1 < w2))
					{
						dp[i][j] = max(dp[i][j], dp[i - 1][k] + arr[j][2]);
					}
				}
			}
		}
		int ans = 0;
        // 取所有层中选择所有面作为顶面的最优解
		for (int i = 1; i <= 200; i++)
		{
			for (int j = 1; j < index; j++)
			{
				ans = max(ans, dp[i][j]);
			}
		}
		cout << "Case " << kase++ << ": maximum height = " << ans << endl;
	}
	return 0;
}

1005:Big Event in HDU(思维+01背包)

题意:有 N N 类物品,都有各自的价值 V i V_i ,每类物品有 M i M_i​ 个,现在需要将这些物品分成两堆,让两堆的总价值相差最小。

范围: 0 < N 50   ,   0 < V i 50   ,   0 < M i 100 0 < N \le 50~,~0< V_i \le 50~,~0 < M_i \le 100

分析:这道题目还是比较套路的,设所有物品的价值之和为 s u m sum ,那么要让两堆物品的总价值相差最小,相当于两堆的总价值尽量往 s u m / 2 sum/2 靠近,因此我们可以在 s u m / 2 sum/2 的背包容量内做 01 01 背包!

(唉,之前西安邀请赛的时候就出了个这样的题目,那时候没想出来是背包,要是我早点做了这题就好了,大家还是要好好训练啊!)

这道题目中这些物品的数量可以有多个,总数量不超过 5000 5000 个,可以把物品拆分出来,用 01 01 背包,但是注意空间需要优化一下,我这里使用两行的 d p dp 数组,因为确定 d p [ i ] [ j ] dp[i][j] 只需要上一层的 d p dp​ 值。(其实是可以优化成一维的,后面的题目会用到)

详见代码。

Code

// 这题直接用cin会TLE,所以我改成了scanf,取消流同步应该也可以
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 5000 + 10;

int n;
int value[MAXN], dp[2][MAXN*25];  // 只用两行,背包的容量最大为50*50*100/2

int main()
{
	while (~scanf("%d", &n), n > 0)
	{
		memset(dp, 0, sizeof(dp));  // 清空数组
		int sum = 0;  // 总价值
		int index = 1;  // 用于拆分物品
		for (int i = 1; i <= n; i++)
		{
			int v, c;
			scanf("%d%d", &v, &c);
			sum += v * c;
            // 拆分
			for (int j = 0; j < c; j++)
			{
				value[index++] = v;
			}
		}
        // 01背包
		for (int i = 1; i < index; i++)
		{
			for (int j = 0; j <= sum / 2; j++)
			{
                // 因为只用两行,所以根据奇偶性判断上一层
				if (j >= value[i])
				{
					dp[i % 2][j] = max(value[i] + dp[!(i % 2)][j - value[i]], dp[!(i % 2)][j]);
				}
				else
				{
					dp[i % 2][j] = dp[!(i % 2)][j];
				}
			}
		}
        // 保证左边不比右边小
		int ans1 = max(dp[0][sum / 2], dp[1][sum / 2]);
		int ans2 = sum - ans1;
		if (ans1 < ans2)
			swap(ans1, ans2);
		printf("%d %d\n", ans1, ans2);
	}
	return 0;
}

1007:免费馅饼(dp)

题意:天上掉 N N​ 个馅饼,分别在 T i T_i​ 时刻掉落在区间 [ 0 , 10 ] [0, 10]​ 之间,一开始人在 5 5​ ,每秒钟可以往左或者往右走一格,或者待在原地。现在问最多能接到多少馅饼?

范围: 0 < N 1 e 5   ,   0 < T i < 1 e 5 0 < N \le 1e5~,~0<T_i<1e5

分析:没想到可以证明的贪心策略,考虑 d p dp

考虑在时间为 i i 的时候当前位置在 j j 的情况是怎么转移来的,只能是在 i 1 i-1 时刻从 j 1 , j , j + 1 j-1,j,j+1 这三个位置过来的。

影响因素有哪些?

  1. 上一秒位置在 j 1 j-1 时的最优解
  2. 上一秒位置在 j j 时的最优解
  3. 上一秒位置在 j + 1 j+1​ 时的最优解

d p [ i ] [ j ] dp[i][j] 表示时刻 i i 在位置 j j 的最优解,答案就是 T T 时刻 11 11 个位置中的最大值。

另外我们使用数组 a r r [ i ] [ j ] arr[i][j] 表示时刻 i i 在位置 j j 时有多少个饼掉下,同一时刻同一位置可能有多个饼。

转移方程:

d p [ i ] [ j ] = m a x ( d p [ i 1 ] [ j 1 ] , m a x ( d p [ i 1 ] [ j ] , d p [ i 1 ] [ j + 1 ] ) ) + a r r [ i ] [ j ] ; dp[i][j] = max(dp[i-1][j-1], max(dp[i-1][j], dp[i-1][j+1])) + arr[i][j];

(上一秒附近三个位置的最大值+该时刻该位置的馅饼掉落数量)

Code

// 提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int n;

int arr[MAXN][12], dp[MAXN][12];

int main()
{
	while (~scanf("%d", &n), n)
	{
        // 清空数组
		memset(arr, 0, sizeof(arr)); 
		memset(dp, 0, sizeof(dp));
		int T = 0, ans = 0;
		for (int i = 0; i < n; i++)
		{
			int t, x;
			scanf("%d%d", &x, &t);
			arr[t][x]++;
			T = max(T, t);  // 有效时间的上限
		}
        // 初始化
		dp[1][4] = arr[1][4];
		dp[1][5] = arr[1][5];
		dp[1][6] = arr[1][6];
        // 根据转移方程计算
		for (int i = 2; i <= T; i++)
		{
			for (int j = 0; j <= 10; j++)
			{
				dp[i][j] = max(dp[i - 1][j - 1], max(dp[i - 1][j], dp[i - 1][j + 1])) + arr[i][j];
			}
		}
        // 取所有点上的最大值
		for (int i = 0; i <= 10; i++)
		{
			ans = max(ans, dp[T][i]);
		}
		printf("%d\n", ans);
	}
	return 0;
}

1008:I NEED A OFFER!(数学+dp)

题意:身上有 N N​ 万美元,现在有 M M​ 个学校可以申请,申请需要花费 a i a_i​ 万元,拿到offer的概率为 b i b_i​ ,问怎么安排至少能拿到一个offer。

范围: 0 N , M 1 e 5   ,   a i 0 \le N,M \le 1e5~, ~a_i​ 整型 ,   b i ,~b_i​ 实型

分析: 01 01 背包的变形,需要进行一些转换。

本题要我们求至少拿到一个offer的最大概率,那么意思就是说要求一个offer都拿不到的最小概率。

01 01​ 背包处理的时候在容量为 V V​ 的情况下最多能取到的价值,我们这里需要资金为 N N​ 的情况下取到的最小概率,其实是差不多的。

d p [ i ] [ j ] dp[i][j] 表示可以选择 1 i 1 \sim i 学校并且资金为 j j 时能够取到的最小概率,注意需要优化空间,不然数组开不下。

转移方程: d p dp​ 数组全部初始化为 1 1​

d p [ i ] [ j ] = m i n ( d p [ i 1 ] [ j ] , d p [ i 1 ] [ j c o s t [ i ] ] ( 1 c h a n c e [ i ] ) ) dp[i][j] = min(dp[i-1][j], dp[i-1][j-cost[i]]*(1-chance[i])) j > = c o s t [ i ] j >= cost[i]​

d p [ i ] [ j ] = d p [ i 1 ] [ j ] dp[i][j] = dp[i-1][j] j < c o s t [ i ] j < cost[i]

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e4 + 10;

int n, m;
double dp[2][MAXN];  // 采用两行dp
int cost[MAXN];  // 申请的花费
double chance[MAXN];  // 拿到offer的概率

int main()
{
	while (cin >> n >> m, n + m)
	{
        // 初始化为1
		for (int i = 0; i <= 1; i++)
		{
			for (int j = 0; j <= n; j++)
			{
				dp[i][j] = 1;
			}
		}
		for (int i = 1; i <= m; i++)
		{
			cin >> cost[i] >> chance[i];
		}
        // 01背包变形
		for (int i = 1; i <= m; i++)
		{
			for (int j = 0; j <= n; j++)
			{
				if (j >= cost[i])
				{
                    // 根据奇偶性判断上一层dp值
					dp[i % 2][j] = min(dp[!(i % 2)][j], dp[!(i % 2)][j - cost[i]] * (1 - chance[i]));
				}
				else
				{
					dp[i % 2][j] = dp[!(i % 2)][j];
				}
			}
		}
        // 1-拿不到offer的最小概率=拿到至少一个offer的最大概率
		double ans = 1 - dp[m % 2][n];
		cout << fixed << setprecision(1) << ans * 100 << "%" << endl;
	}
	return 0;
}

1009:FATE(三维完全背包)

题意:升级需要 N N​ 点经验,还剩下 M M​ 点忍耐度,有 K K​ 种怪物,数量无限,杀掉每种怪物能得到 a i a_i​ 点经验并且花费 b i b_i​ 点忍耐度,现在问是否能够在杀不超过 S S​ 个怪物的条件下升级,如果能则输出升级后还能保留的最大忍耐度,否则输出 1 -1​

范围: 0 < N , M , K , S < 100   ,   0 < a i , b i < 20 0 < N, M, K, S < 100~, ~0 < a_i, b_i < 20

分析:又是背包的变形,看到怪物的数量是无限的,那么应该想到是完全背包,不过多了一个维度变成三维,稍微改一点就可以。

除此以外,如果能够升级的话我们还是需要输出能保留的最大忍耐度,这个可以在我们求完 d p dp 数组之后遍历背包容量 0 M 0\sim M ,一旦当前最大经验值大于所需要的经验值,那么剩下的忍耐度就是答案。

影响的因素有哪些?

  1. 选择区间
  2. 剩余背包容量
  3. 剩余能杀的怪物数量

d p [ i ] [ j ] [ k ] dp[i][j][k] 表示在 1 i 1\sim i 物品中选择、当前的容量为 j j 并且还能选 k k 个物品的最优解。

转移方程:

d p [ i ] [ j ] [ k ] = m a x ( d p [ i 1 ] [ j ] [ k ] , d p [ i ] [ j c o s t [ i ] ] [ k 1 ] + v a l u e [ i ] ) dp[i][j][k] = max(dp[i-1][j][k], dp[i][j-cost[i]][k-1]+value[i])​ j > = c o s t [ i ] j >= cost[i]​ 并且 k > 0 k > 0​

d p [ i ] [ j ] [ k ] = d p [ i 1 ] [ j ] [ k ] dp[i][j][k] = dp[i-1][j][k]​ 其他情况

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;

int n, m, k, s;

int dp[MAXN][MAXN][MAXN];
int cost[MAXN], value[MAXN];

int main()
{
	while (cin >> n >> m >> k >> s)
	{
		memset(dp, 0, sizeof(dp));  // 清空数组
		for (int i = 1; i <= k; i++)
		{
			cin >> value[i] >> cost[i];
		}
        // 三维完全背包
		for (int i = 1; i <= k; i++)
		{
			for (int j = 0; j <= m; j++)
			{
				for (int t = 0; t <= s; t++)
				{
                    // 保证容量足够并且还可以杀怪
					if (j >= cost[i] && t)
					{
						dp[i][j][t] = max(dp[i - 1][j][t], dp[i][j - cost[i]][t - 1] + value[i]);
					}
					else
					{
						dp[i][j][t] = dp[i - 1][j][t];
					}
				}
			}
		}
        // 先判断能不能升级
		if (dp[k][m][s] < n)
		{
			cout << -1 << endl;
			continue;
		}
        // 再从0~m遍历背包得到最大剩余的忍耐度
		int ans = 0;
		for (int i = 0; i <= m; i++)
		{
			if (dp[k][i][s] >= n)
			{
				ans = m - i;
				break;
			}
		}
		cout << ans << endl;
	}
	return 0;
}

1010:Coins(多重背包)

题意:钱包中有 N N​ 种硬币,面值为 A i A_i​ 的有 C i C_i​ 个,现在问通过组合能够得到多少个不大于 M M​ 的金额。

范围: 1 N 100   ,   M 1 e 5   ,   1 A i 1 e 5   ,   1 C i 1000 1 \le N \le 100~, ~M \le 1e5~,~1\le A_i \le 1e5~,~ 1 \le C_i \le 1000​

分析:仍然是背包的背景,这题是多重背包模板题,可以学习一下。

按我的理解多重背包就是介于 01 01 背包与完全背包之间的背包问题,有 n n​ 个物品,每个物品都有自己的价值还有重量,除此以外还有数量的限制,这个就是最主要的不同之处了。

01 01​ 背包每种物品只有1个,完全背包每种物品有无数个。

经过处理之后多重背包能够转换成 01 01​ 背包和完全背包。

假设一个物品的数量限制为 k [ i ] k[i] ,那么我们可以选择这个物品 0 k [ i ] 0\sim k[i] 个。

分类讨论:

  1. k [ i ] w e i g h t [ i ] > = V k[i]*weight[i] >= V​ (背包总容量),那么在这种情况下实际上是供大于求的,可以把这个数量看作是无限个,那么问题就转化成完全背包了。
  2. 否则,我们可以按照二进制将这 k [ i ] k[i] 个物品拆分成 1 , 2 , 4 , 8... 1,2,4,8... 个,那么无论最终这个物品选择了多少次,我们都可以用若干个拆分出来的物品组合而成。
    为什么?因为我们选择的物品数量 n u m num 一定不大于 k [ i ] k[i] ,且 n u m num 可以用二进制表示,而其中二进制每一位上的数子我们都已经单独拆分出来了,可以直接进行组合,那么问题转化成 01 01 背包了。

Notice:本题不得不使用优化成一维数组的背包了!

若使用一维数组,那么对于 01 01 背包来说,需要从容量大往小进行更新 d p dp 数组,此时数组左边的部分就是相当于上一层的部分,右边的部分就是本层的部分;对于完全背包来说,因为完全背包需要利用本层更新的 d p dp 数组,因此需要从容量小往大进行更新 d p dp​ 数组。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;
const int MAXM = 100000 + 10;
const int INF = 0x3f3f3f3f;

int n, m;

int value[MAXN], cnt[MAXN];
int dp[MAXM];  // 一维数组!

// 01背包
void zeroOnePack(int weight, int value)
{
	for (int i = m; i >= weight; i--) // 01背包的一维从右往左,使用上层的dp
	{
		dp[i] = max(dp[i], dp[i - weight] + value);
	}
}

// 完全背包
void completePack(int weight, int value)
{
	for (int i = weight; i <= m; i++) // 完全背包的一维从左往右,使用本层的dp
	{
		dp[i] = max(dp[i], dp[i - weight] + value);
	}
}

// 多重背包
void multiplePack(int weight, int value, int number)
{
    // 供大于求,转化成完全背包
	if (m <= number * weight)
	{
		completePack(weight, value);
	}
    // 否则拆分物品,转化成01背包
	else
	{
        // 将0~number二进制每一位上的1拆分出来
		int k = 1;
		while (k <= number)
		{
			zeroOnePack(k * weight, k * value);
			number -= k;
			k *= 2;
		}
		zeroOnePack(number * weight, number * value);
	}
}

int main()
{
	while (cin >> n >> m, n + m)
	{
		for (int i = 1; i <= n; i++)
		{
			cin >> value[i];
		}
		for (int i = 1; i <= n; i++)
		{
			cin >> cnt[i];
		}
        // 初始化数组
		for (int i = 0; i <= m; i++)
		{
			dp[i] = -INF;
		}
        // 容量为0的dp数组需要置为0
		dp[0] = 0;
        // 多重背包
		for (int i = 1; i <= n; i++)
		{
			multiplePack(value[i], value[i], cnt[i]);
		}
        // 统计答案
		int ans = 0;
		for (int i = 1; i <= m; i++)
		{
			if (dp[i] > 0)
				ans++;
		}
		cout << ans << endl;
	}
	return 0;
}

1012:Common Subsequence(dp)

题意:给两个字符串 s t r 1 , s t r 2 str1, str2​ ,求两者的最长公共子序列长度。

范围:没有明确指出,估计都是少于 1000 1000

分析:经典的 L C S LCS 问题,必须掌握。

影响的因素有哪些?

  1. 字符串 s t r 1 str1​ 的指针位置
  2. 字符串 s t r 2 str2​ 的指针位置

d p [ i ] [ j ] dp[i][j]​ 表示 s t r 1 str1​ 当前匹配位置为 i i​ s t r 2 str2​ 当前匹配位置为 j j​ 时的最长公共子序列。

如果 s t r 1 [ i ] = = s t r 2 [ j ] str1[i] == str2[j]​ 的话,那么该元素一定是位于两者的 L C S LCS​ 中的,因此只需要在剩下的串中求 L C S LCS​ 即可;

否则,那么剩下串的 L C S LCS​ 只能在 d p [ i 1 ] [ j ] dp[i-1][j]​ d p [ i ] [ j 1 ] dp[i][j-1]​ 中产生。

转移方程:

d p [ i ] [ j ] = d p [ i 1 ] [ j 1 ] + 1 dp[i][j] = dp[i-1][j-1]+1 s t r 1 [ i ] str1[i] == s t r 2 [ i ] str2[i]

d p [ i ] [ j ] = m a x ( d p [ i 1 ] [ j ] , d p [ i ] [ j 1 ] ) dp[i][j] = max(dp[i-1][j], dp[i][j-1])​ 其他情况

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000 + 10;
const int INF = 0x3f3f3f3f;

int n;

int dp[MAXN][MAXN];

int LCS(string str1, string str2)
{
	int m = str1.length(), n = str2.length();
	int i, j;
	dp[0][0] = 0;
	for (i = 1; i <= m; ++i)
	{
		dp[i][0] = 0;
	}
	for (i = 1; i <= n; ++i)
	{
		dp[0][i] = 0;
	}
	for (i = 1; i <= m; ++i)
	{
		for (j = 1; j <= n; ++j)
		{
			if (str1[i - 1] == str2[j - 1])
			{
				dp[i][j] = dp[i - 1][j - 1] + 1;
			}
			else if (dp[i - 1][j] > dp[i][j - 1])
			{
				dp[i][j] = dp[i - 1][j];
			}
			else
			{
				dp[i][j] = dp[i][j - 1];
			}
		}
	}
	return dp[m][n];
}

int main()
{
	string str1, str2;
	while (cin >> str1 >> str2)
	{
		cout << LCS(str1, str2) << endl;
	}
	return 0;
}

1013:搬寝室(贪心+dp)

题意:有 N N 件物品,重量为 a r r [ i ] arr[i] ,只需要搬其中 2 k 2*k 件,每次需要选择 2 2 件物品,即需要搬 k k 趟,每次搬运的花费是这两件物品重量差的平方,即 ( a r r [ i ] a r r [ j ] ) 2 (arr[i]-arr[j])^2 。问至少需要花费多少的代价才能搬完 2 k 2*k​ 件物品。

范围: 2 2 k n < 2000   ,   a r r [ i ] < 2 15 2 \le 2*k \le n < 2000~,~ arr[i] < 2^{15}​

分析:一开始确实感觉有点无从下手,但是观察发现每次选择两个物品的时候,一定要选择重量相邻的两个物品,否则不可能是最优解。

比如有重量为 a , b , c , d a, b, c, d 的四个物品,重量递增,如果选了 c c ,要么选 b b 或者选 d d ,不能选 a a

因此我们要先把物品按照重量进行排序,这样对我们接下来的 d p dp​ 有帮助。

影响的因素有哪些?

  1. 物品选择区间
  2. 还需要选择的物品数量

d p [ i ] [ j ] dp[i][j] 表示处理 1 i 1\sim i 这些物品并且需要选 j j​ 次物品时的最优解

转移方程:

d p [ i ] [ j ] = m i n ( d p [ i 1 ] [ j ] , d p [ i 2 ] [ j 1 ] + ( a r r [ i ] a r r [ i 1 ] ) 2 ) dp[i][j] = min(dp[i-1][j], dp[i-2][j-1]+(arr[i]-arr[i-1])^2)

(不选择和选择 i i i 1 i-1​ 这一对物品两种情况中的最优解)

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2000 + 10;
const int INF = 0x3f3f3f3f;

int n, k;

int arr[MAXN], dp[MAXN][MAXN / 2]; 

int main()
{
	while (cin >> n >> k)
	{
        // 初始化
		dp[0][0] = 0;
		for (int i = 0; i <= n; i++)
		{
			for (int j = 1; j <= k; j++)
			{
				dp[i][j] = INF;  // dp[i][j]的初始值要够大,不然会wa的
			}
		}
		for (int i = 1; i <= n; i++)
		{
			cin >> arr[i];
		}
		sort(arr + 1, arr + 1 + n);  // 先排序
        // 根据转移方程更新dp数组
		for (int i = 2; i <= n; i++)
		{
			for (int j = 1; j <= k; j++)
			{
				dp[i][j] = min(dp[i - 1][j], dp[i - 2][j - 1] + (arr[i] - arr[i - 1]) * (arr[i] - arr[i - 1]));
			}
		}
		cout << dp[n][k] << endl;
	}
	return 0;
}

1014:Humble Numbers(数学+dp预处理)

题意:输出第 n n​ 个丑数。

范围: n 5842 n \le 5842

分析:任何一个数分解到最后都是由一些质数或者 1 1​ 相乘。基于此,题目中设一个数的质数因子在 2 , 3 , 5 , 7 2,3,5,7​ 之中,所以,这个数应当从 1 1​ 开始,不断乘以 2 , 3 , 5 , 7 2,3,5,7​ 获得。

因此我们可以从 1 1​ 开始预处理出所有的丑数,直接输出答案即可。

d p [ i ] = m i n ( 2 d p [ i n d e x 2 ] , 3 d p [ i n d e x 3 ] , 5 d p [ i n d e x 5 ] , 7 d p [ i n d e x 7 ] ) dp[i] = min(2*dp[index2], 3*dp[index3], 5*dp[index5], 7*dp[index7])

详见代码。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 6000 + 10;

int n;

int ans[MAXN];

int main()
{
    // dp预处理
	int index2 = 1, index3 = 1, index5 = 1, index7 = 1;
	ans[1] = 1;
	for (int i = 2; i <= 6000; i++)
	{
		ans[i] = min(2 * ans[index2], min(3 * ans[index3], min(5 * ans[index5], 7 * ans[index7])));
        // 4个if,如果出现重复的可以跳过
        // 比如6,可以被2和3整除,只保留一个,都往后走
		if (ans[i] == 2 * ans[index2])
			index2++;
		if (ans[i] == 3 * ans[index3])
			index3++;
		if (ans[i] == 5 * ans[index5])
			index5++;
		if (ans[i] == 7 * ans[index7])
			index7++;
	}
	while (cin >> n, n)
	{
		cout << "The " << n;
        // 其实只要知道前13个数字就可以了,下面是我的写法
		if(n%10 == 1 && n%100 != 11) cout << "st";
		else if(n%10 == 2 && n%100 != 12) cout << "nd";
		else if(n%10 == 3 && n%100 != 13) cout << "rd";
		else cout << "th";
		cout << " humble number is " << ans[n] << "." << endl;
	}
	return 0;
}

1016:悼念512汶川大地震遇难同胞——珍惜现在,感恩生活(多重背包)

题意:给 N N​ 元钱去买米,市场有 M M​ 种米,每种米价格、重量以及数量分别为 p i h i c i p_i、h_i、c_i​ ,现在问最多能买多少斤的米。

范围: 1 N , M 100   ,   1 p , c 20   ,   1 h 200 1 \le N,M \le 100~,~1 \le p,c \le 20~,~1\le h \le 200

分析:多重背包模板题,与 1010 1010​ 类似。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;
const int MAXM = 100000 + 10;
const int INF = 0x3f3f3f3f;

int n, m;

int cost[MAXN], value[MAXN], cnt[MAXN];
int dp[MAXM];

void zeroOnePack(int weight, int value)
{
	for (int i = m; i >= weight; i--) // 01背包的一维从右往左,使用上层的dp
	{
		dp[i] = max(dp[i], dp[i - weight] + value);
	}
}

void completePack(int weight, int value)
{
	for (int i = weight; i <= m; i++) // 完全背包的一维从左往右,使用本层的dp
	{
		dp[i] = max(dp[i], dp[i - weight] + value);
	}
}

void multiplePack(int weight, int value, int number)
{
	if (m <= number * weight)
	{
		completePack(weight, value);
	}
	else
	{
		int k = 1;
		while (k <= number)
		{
			zeroOnePack(k * weight, k * value);
			number -= k;
			k *= 2;
		}
		zeroOnePack(number * weight, number * value);
	}
}

int main()
{
	int T;
	cin >> T;
	while (T--)
	{
		cin >> m >> n;
		for (int i = 1; i <= n; i++)
		{
			cin >> cost[i] >> value[i] >> cnt[i];
		}
		for (int i = 0; i <= m; i++)
		{
			dp[i] = -INF;
		}
		dp[0] = 0;
		for (int i = 1; i <= n; i++)
		{
			multiplePack(cost[i], value[i], cnt[i]);
		}
		cout << dp[m] << endl;
	}
	return 0;
}

1017:Piggy-Bank(完全背包)

题意:存钱罐自身重量为 E E​ ,投入硬币后总重量为 F F​ ,现在有 N N​ 种硬币,数量无限,每种硬币都有自身的价值 P i P_i​ 与重量 W i W_i​ ,问存钱罐里面最少可能有多少钱。

范围: 1 E , F 1 e 4   ,   1 N 500   ,   1 P i 5 e 4   ,   1 W i 1 e 4 1 \le E,F \le 1e4~,~1 \le N \le 500~,~1\le P_i \le 5e4~,~1 \le W_i \le 1e4​

分析:完全背包变形,从求背包最大价值变成求最小价值,改一下 d p dp 数组的初始值以及转移方程时的 m a x > m i n max->min 就可以了。

d p [ i ] [ j ] dp[i][j] 表示可以选择 1 i 1\sim i 中的物品并且容量为 j j 时的最小值

转移方程:

d p [ i ] [ j ] = m i n ( d p [ i 1 ] [ j ] , d p [ i ] [ j w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = min(dp[i-1][j], dp[i][j-weight[i]]+value[i]) j > = w e i g h t [ i ] j >= weight[i]​

d p [ i ] [ j ] = d p [ i 1 ] [ j ] dp[i][j] = dp[i-1][j] j < w e i g h t [ i ] j < weight[i]

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 500 + 10;
const int INF = 0x3f3f3f3f;

int n, w1, w2;

int value[MAXN], weight[MAXN];
int dp[MAXN][10010];

int main()
{
	int T;
	cin >> T;
	while (T--)
	{
		cin >> w1 >> w2 >> n;
		int tot = w2 - w1;  // 真正的背包容量
		for (int i = 1; i <= n; i++)
		{
			cin >> value[i] >> weight[i];
		}
        // 注意初始化
		for (int i = 0; i <= n; i++)
		{
			for (int j = 0; j <= tot; j++)
			{
				dp[i][j] = INF;
			}
		}
        // 完全背包变形,并且是恰好背包
		dp[0][0] = 0;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 0; j <= tot; j++)
			{
				if (j >= weight[i])
				{
					dp[i][j] = min(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
				}
				else
				{
					dp[i][j] = dp[i - 1][j];
				}
			}
		}
		if (dp[n][tot] >= INF)
		{
			cout << "This is impossible." << endl;
		}
		else
		{
			cout << "The minimum amount of money in the piggy-bank is " << dp[n][tot] << "." << endl;
		}
	}
	return 0;
}

1018:Free DIY Tour(dp+路径记录)

题意:需要从城市 1 1 到 城市 N + 1 N+1 ,每个城市都有自己的兴趣点数,每个城市能单向到达的城市已经给出,问能获得的最大兴趣点数是多少并且输出路径。

范围: 2 N 100 2 \le N \le 100

分析:本题的 d p dp 思路还是清晰的,细节比较多。

考虑每个城市只能从直接到达该城市的城市来,那么我们只需要取这些城市中的最优解就可以了。

d p [ i ] dp[i] 表示从城市 1 1 旅行到城市 i i​ 所获得的最大兴趣点

转移方程:

d p [ i ] = m a x ( d p [ i ] , d p [ j ] + a r r [ i ] ) dp[i] = max(dp[i], dp[j]+arr[i]) 城市 j j 能直接到达城市 i i

此外我们用数组 p r e pre 辅助, p r e [ i ] pre[i] 表示城市 i i 最优解的上一个城市序号,然后使用栈倒序输出即可。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;

int n, m;

int arr[MAXN], dp[MAXN], pre[MAXN];

vector<int> E[MAXN];  // 保存边

stack<int> st;  // 用于输出路径

int main()
{
	int T;
	cin >> T;
	int kase = 1;
	while (T--)
	{
		memset(dp, 0, sizeof(dp));  // 清空数组
		cin >> n;
		for (int i = 1; i <= n; i++)
		{
			cin >> arr[i];
			E[i].clear();  // 清空边集
		}
		arr[n + 1] = 0;  // 1和n+1的兴趣点都是0
		cin >> m;
		for (int i = 1; i <= m; i++)
		{
			int u, v;
			cin >> u >> v;
			E[v].push_back(u);
		}
        // 根据转移方程进行更新dp数组,同时记录前继
		for (int i = 1; i <= n + 1; i++)
		{
			for (auto x : E[i])
			{
				if (dp[x] + arr[i] > dp[i])
				{
					dp[i] = dp[x] + arr[i];
					pre[i] = x;
				}
			}
		}
		cout << "CASE " << kase++ << "#" << endl;
		cout << "points : " << dp[n + 1] << endl;
		cout << "circuit : ";
        // 从后往前将前继一一加入栈
		int now = n + 1;
		while (now)
		{
			st.push(now);
			now = pre[now];
		}
        // 利用栈倒叙输出路径
		while (!st.empty())
		{
			if (st.top() == n + 1)
				cout << 1;
			else
				cout << st.top();
			st.pop();
			if (!st.empty())
				cout << "->";
		}
		cout << endl;
        // 输出块之间有空行
		if (T)
			cout << endl;
	}
	return 0;
}

1020:最大报销额(01背包+细节)

题意:报销发票。允许报销的发票类型 A A​ 类、 B B​ 类、 C C​ 类,要求每张发票的总额不得超过 1000 1000​ 元,每张发票中单项物品的价值不得超过 600 600​ 元。现在需要在给出的一堆发票中找出满足报销条件的最大报销额。

范围: N 30 N \le 30

分析:应该可以看出来是 01 01 背包吧,但是还有一些细节需要优化与处理。

首先题目中说的单项物品的价值不能超过 600 600 元,意思是**同一类**物品的价值不能超过 600 600

然后 01 01 背包的列是容量,是整数,而本题的容量是浮点数,但是注意到只保留小数 2 2 位,因此可以全部扩大 100 100 倍,变成整数,输出答案的时候再除 100 100

最后就是解析发票上的每个条目,这里我使用 s t r i n g string 读入,再使用 s t r i n g s t r e a m stringstream 来分离类别以及金额。

Notice:一维数组一维数组!二维开不下!

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 30 + 10;

int q, n;

// 最多报销MAXN张1000元的发票,转成int需要再乘100
int value[MAXN], dp[1000 * MAXN * 100];

int main()
{
	double t;
	while (cin >> t >> n, n)
	{
		q = t * 100;  // q是整数
		memset(dp, 0, sizeof(dp));  // 清空数组
		int index = 1;
		for (int i = 1; i <= n; i++)
		{
			int m;
			cin >> m;
            // 分别保存A、B和C类项目的总金额
			double sum1 = 0, sum2 = 0, sum3 = 0;
			int flag = 1;  // 判断该发票是否非法
			for (int j = 1; j <= m; j++)
			{
				string str;
				cin >> str;
                // 分离类型和金额
				stringstream ss(str);
				char ch, c;
				ss >> ch >> c;
				double v;
				ss >> v;
                // 超过600,非法
				if (v > 600)
					flag = 0;
                // 分类讨论
				if (ch == 'A')
					sum1 += v;
				else if (ch == 'B')
					sum2 += v;
				else if (ch == 'C')
					sum3 += v;
				else
					flag = 0;
			}
            // 满足条件才成为背包问题的一个物品
			if (flag && sum1 + sum2 + sum3 <= 1000 && sum1 <= 600 && sum2 <= 600 && sum3 <= 600)
			{
				value[index++] = (sum1 + sum2 + sum3) * 100;
			}
		}
        // 01背包,重量和价值是相同的
		for (int i = 1; i < index; i++)
		{
			for (int j = q; j >= value[i]; j--)
			{
				dp[j] = max(dp[j], dp[j - value[i]] + value[i]);
			}
		}
        // 注意精度控制,还要除以100
		cout << fixed << setprecision(2) << dp[q] * 1.0 / 100 << endl;
	}
	return 0;
}

三、hard

1019:Regular Words(dp+预处理+高精)

题意:一个字符串只含有 A , B , C A,B,C​ 三种字符,定义 A ( w ) A(w)​ 为字符串中 A A​ 的数量, B ( w ) B(w)​ 为字符串中 B B​ 的数量, C ( w ) C(w)​ 为字符串中 C C​ 的数量。称一个字符串为合法,当字符串的所有前缀都满足 A ( w ) B ( w ) C ( w ) A(w) \ge B(w) \ge C(w)​ 。现在问长度为 3 N 3N​ 的字符串中有多少满足条件的字符串。

范围: 0 N 60 0 \le N \le 60

分析:考虑 d p dp ,我们知道变化量是 A ( w ) , B ( w ) , C ( w ) A(w), B(w), C(w) ,那么就可以用 ( A , B , C ) (A, B, C) 来代表当前的状态。

当前状态 ( A , B , C ) (A, B, C) 只能是从 ( A 1 , B , C ) ( A , B 1 , C ) (A-1, B, C)、(A, B-1, C) 以及 ( A , B , C 1 ) (A, B, C-1) 这三种状态转移而来的。

d p [ i ] [ j ] [ k ] dp[i][j][k] 表示 A ( w ) = i , B ( w ) = j , C ( w ) = k A(w)=i,B(w)=j,C(w)=k 时的最大方案数

转移方程:

d p [ i ] [ j ] [ k ] = d p [ i 1 ] [ j ] [ k ] + d p [ i ] [ j 1 ] [ k ] + d p [ i ] [ j ] [ k 1 ] ; dp[i][j][k] = dp[i-1][j][k]+dp[i][j-1][k]+dp[i][j][k-1];

注意到这道题目的 n n 很小,而且很多组输入,那么我们就可以打表来提高程序的效率。

此外,在 n n 到达 60 60 的时候方案数已经非常多了,并且题目没有取模的需求,那么就需要上高精了。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 60 + 10;
const int L = 110;  // 高精最多能处理的位数

int n;

string dp[MAXN][MAXN][MAXN];  // 高精表示的dp

// 高精度相加,只限两个非负整数相加
string add(string a, string b) 
{
	string ans;
	int na[L] = {0}, nb[L] = {0};
	int la = a.size(), lb = b.size();
	for (int i = 0; i < la; i++)
		na[la - 1 - i] = a[i] - '0';
	for (int i = 0; i < lb; i++)
		nb[lb - 1 - i] = b[i] - '0';
	int lmax = la > lb ? la : lb;
	for (int i = 0; i < lmax; i++)
		na[i] += nb[i], na[i + 1] += na[i] / 10, na[i] %= 10;
	if (na[lmax])
		lmax++;
	for (int i = lmax - 1; i >= 0; i--)
		ans += na[i] + '0';
	return ans;
}

int main()
{
    // 将dp数组初始化为0,因为需要上高精,所以是字符串“0”
	for (int i = 0; i < MAXN; i++)
	{
		for (int j = 0; j < MAXN; j++)
		{
			for (int k = 0; k < MAXN; k++)
			{
				dp[i][j][k] = "0";
			}
		}
	}
    // 需要先对这三种情况进行预处理
	dp[1][0][0] = dp[1][1][0] = dp[1][1][1] = "1";
	// 根据转移方程进行更新dp数组
    for (int i = 2; i < MAXN; i++)
	{
		for (int j = 0; j <= i; j++)
		{
			for (int k = 0; k <= j; k++)
			{
				if (i - 1 >= j)
				{
					dp[i][j][k] = add(dp[i][j][k], dp[i - 1][j][k]);
				}
				if (j - 1 >= k)
				{
					dp[i][j][k] = add(dp[i][j][k], dp[i][j - 1][k]);
				}
				if (k - 1 >= 0)
				{
					dp[i][j][k] = add(dp[i][j][k], dp[i][j][k - 1]);
				}
			}
		}
	}
    // 直接输出答案
	while (cin >> n)
	{
		cout << dp[n][n][n] << endl;
		cout << endl;
	}
	return 0;
}

1021:Largest Rectangle in a Histogram(单调栈)

题意:给出含有 N N​ 个宽度为 1 1​ ,高度为 h i h_i​ 的矩形的直方图,问该直方图中最大的矩形面积是多少?

范围: 1 N 1 e 5     0 h i 1 e 9 1 \le N \le 1e5~,~0 \le h_i \le 1e9

分析:首先我们可以知道,最大的矩形是直方图中连续的一段矩形拼接出来的,其高度取决于这一段矩形中的最低高度,那么一个比较直观的方法就是对于每一个宽度为 1 1​ 的矩形,我们把它看做是大矩形中高度最低的矩形,然后看看能往左边有右边分别拓展多长,这样我们就能计算出该大矩形的宽度以及高度,更新答案即可。

那具体怎么往左右两边拓展呢?使用单调栈。

单调栈,栈中的元素时递增的,当读入的数比栈顶的数小时,将栈顶元素弹出直到满足栈中元素递增,在栈顶元素弹出时,说明该弹出的元素能够拓展到的位置最多只能到当前位置。

因此我们可以利用单调栈预处理出第 i i 个矩形往左边拓展的最长距离 l [ i ] l[i] 以及往右拓展的最长距离 r [ i ] r[i] ,之后遍历每个矩形,计算面积,更新答案。

Notice:注意开 long long

Code

#include <bits/stdc++.h>
#define int long long  // 开longlong
using namespace std;

const int MAXN = 1e5 + 10;

int n;
int arr[MAXN], l[MAXN], r[MAXN];
stack<int> s;  // 单调栈

signed main()
{
	while (cin >> n && n)
	{
		for (int i = 1; i <= n; i++)
			cin >> arr[i];
        // 最左边以及最右边设置成最低高度,方便后面统一操作
		arr[0] = arr[n + 1] = -1;
        // 处理右边拓展长度r[i]数组
		for (int i = 1; i <= n + 1; i++)
		{
            // 如果比栈顶元素小则弹出元素,记录长度
			while (!s.empty() && arr[i] < arr[s.top()])
			{
				r[s.top()] = i;
				s.pop();
			}
			s.push(i);  // 加入单调栈
		}
        // 清空栈
		while (!s.empty())
			s.pop();
        // 处理左边拓展长度l[i]数组
		for (int i = n; i >= 0; i--)
		{
			while (!s.empty() && arr[i] < arr[s.top()])
			{
				l[s.top()] = i;
				s.pop();
			}
			s.push(i);
		}
		while (!s.empty())
			s.pop();
        // 遍历每个矩形计算最大面积统计答案
		int ans = 0;
		for (int i = 1; i <= n; i++)
		{
			int R = r[i] - 1, L = l[i] + 1;
			ans = max(ans, arr[i] * (R - L + 1));
		}
		cout << ans << endl;
	}
}

1022:City Game(最大1矩阵)

题意:给 N M N*M​ 的城市分布图,包含 F F​ R R​ ,分别表示未占用与占用,现在要求出未占用矩形的最大面积 3 *3​

范围: N , M 1000 N,M \le 1000

分析:经典 d p dp 问题,最大 1 1 矩阵,需要掌握。

预处理高度 h h 数组, h [ i ] [ j ] h[i][j] 表示点 ( i , j ) (i, j) 上面连续 1 1 的高度。

有了高度数组,我们可以在 O ( n ) O(n) 的时间内处理出一行中所有点 ( i , j ) (i, j) 左边和右边连续 1 1​ 的长度,同时满足这些点的高度不低于该点的高度。

在这里插入图片描述

只要左边的高度不低于当前点的高度,那么就可以继续往左拓展,往右同理,这样就可以计算出当前点可以拓展的长度。

除此以外,我们可以利用面前求出来的 l l r r 数组,如果当前数字 i i 比前一个数字 i 1 i-1 小,那么一定至少可以拓展到 l [ i 1 ] l[i-1] 的位置,然后继续判断是否可以再往左拓展,右边同理,因此算出该行所有点的 l l r r 数组是 O ( n ) O(n)​ 的。

程序中虽然是双重循环,但是实际上遍历 l l r r 数组的总次数很少,因此跑得快。

那么我们处理出了高度 h h 数组、左右拓展数组 l l r r ,那么接下来我们只需要双重循环遍历每个点,左右拓展之后的宽度*该点的高度=该点所在当前矩形的面积,更新答案即可。

Notice:本题推荐使用 s c a n f scanf ,或者像我使用 C + + C++ 关闭流同步也可以。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000 + 10;

int n, m;

int g[MAXN][MAXN], h[MAXN][MAXN], l[MAXN], r[MAXN];

int main()
{
    // 关闭流同步
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int T;
	cin >> T;
	while (T--)
	{
		cin >> n >> m;
		for (int i = 1; i <= n; i++)
		{
			for (int j = 1; j <= m; j++)
			{
				char ch;
				cin >> ch;
                // 转换成01矩阵
				g[i][j] = ch == 'R' ? 0 : 1;
                // 处理高度数组
				if (g[i][j])
					h[i][j] = h[i - 1][j] + 1;
				else
					h[i][j] = 0;
			}
		}
		int ans = 0;
		for (int i = 1; i <= n; i++)
		{
            // 对于每行初始化l和r数组为自身
			for (int j = 1; j <= m; j++)
			{
				l[j] = r[j] = j;
			}
            // 设置两边监视哨,方便后序操作统一
			h[i][0] = h[i][m + 1] = -1;
            // 处理该行所有点的l数组
			for (int j = 1; j <= m; j++)
			{
				while (h[i][j] <= h[i][l[j] - 1])
				{
					l[j] = l[l[j] - 1];
				}
			}
            // 处理该行所有点的r数组
			for (int j = 1; j <= m; j++)
			{
				while (h[i][j] <= h[i][r[j] + 1])
				{
					r[j] = r[r[j] + 1];
				}
			}
            // 更新答案
			for (int j = 1; j <= m; j++)
			{
				ans = max(ans, (r[j] - l[j] + 1) * h[i][j]);
			}
		}
		cout << ans * 3 << endl;
	}
	return 0;
}

【END】感谢观看!

发布了44 篇原创文章 · 获赞 17 · 访问量 9089

猜你喜欢

转载自blog.csdn.net/qq_41765114/article/details/104091528