[2019年国庆专题训练] dp专题训练

先挖坑,以后慢慢填。。。

文章目录

  • T1.生日礼物
  • T2.大理石
    • 题目描述
    • 题解
    • 参考代码
  • T3.猜数字
    • 题目描述
    • 题解
    • 参考代码
  • T4.加分二叉树
    • 题目描述
    • 题解
    • 参考代码
  • T5.数字计数
    • 题目描述
    • 题解
    • 参考代码
  • T6.逛公园
  • T7.征途
    • 题目描述
    • 题解
    • 参考代码
  • T8.股票交易
  • T9.麻将
  • T10.太空梯
    • 题目描述
    • 题解
    • 参考代码
  • 后记

T1.生日礼物

T2.大理石

题目描述

题目描述
(256MiB / 4000ms)
林老师是一位大理石收藏家,他在家里收藏了n块各种颜色的大理石,第i块大理石的颜色为ai。但是林老师觉得这些石头在家里随意摆放太过凌乱,他希望把所有颜色相同的石头放在一起。换句话说,林老师需要对现有的大理石重新进行排列,在重新排列之后,对于每一个颜色j,如果最左边的颜色为j的大理石是第l块大理石,最右边的颜色为j的大理石是第r块大理石,那么从第l块大理石到第r块大理石,这些石头的颜色都为j。
由于这些大理石都比较重,林老师无法承受这些大理石的重量太久,所以他每次搬运只能交换相邻的两块大理石。请问,林老师最少需要进行多少次搬运?

输入格式
第一行输入一个数字n(2≤n≤4*10^5),表示大理石的总数。
第二行输入n个数字a1,a2…,an(1≤ ai ≤20)表示第i块大理石的颜色为ai

输出格式
输出林老师最少需要搬运的次数。

样例输入输出
Sample Input 1
7
3 4 2 3 4 2 2
Sample Output 1
3

Sample Input 2
5
20 1 14 10 2
Sample Output 2
0

Sample Input 3
13
5 5 4 4 3 5 7 6 5 4 4 6 5
Sample Output 3
21
加粗样式

题解

首先看到这道题时,第一反应就是搜索,但当我们看到数据范围时,就傻眼了2≤n≤4*10^5,与之对比很明显的时颜色的范围1≤ ai ≤20,只有20种颜色,于是,我们可以非常容易地想到用状压DP(此处不再赘述)

有了这个思路,我们可以很容易地想到用二进制来表示当且成堆的大理石的状态,用20位数字表示20种颜色是否全部挪到位了,全部到位则为1,否则为0.复杂度为2^20 = 1048576 ≈ 1e6,显然能过

接下来,我们寻找它的状态转移方程. 与很多状态转移方程类似,

d p [ i + ( 1 < < j ) ] = m i n ( d p [ i + ( 1 < < j ) ] , d p [ i ] + c o s t ) , d p [ 0 ] = 0 dp[i + (1 << j)] = min ( dp[i + (1 << j)], dp[i] + cost ),dp[0] = 0 dp[i+(1<<j)]=min(dp[i+(1<<j)],dp[i]+cost)dp[0]=0


剩下的工作就只剩计算cost
根据题目描述可以知道,大理石只能交换相邻的两个,即,只能与 i + 1 或 i - 1 交换,稍微思考一下就可以发现,i 与 i + 1 交换即是 i + 1 与 (i + 1) - 1交换,所以,我们只用讨论一个方向即可(接下来以往左移为例

由于只能与相邻的大理石进行交换,所以,除了交换的两个大理石,其他大理石的相对位置是没有发生改变的,那么,我们就可以预处理出每两种颜色交换所需要的移动次数,并用一个cost[][]数组存下来,以便后来的dp计算直接调用

具体来说,cost[i][j]表示把所有颜色为j的大理石挪到所有颜色为i的大理石之前所需要的花费,即统计对于所有颜色为j的大理石而言,在它之前的颜色为i的大理石的个数,只需要双重循环累加即可

最后,枚举每一种情况并计算其花费就可以了

参考代码

#include 
#include 
using namespace std;
#define INf 1ll << 60 	//注意一下范围,(1 << 31) - 1过不了
#define LL long long

const int N = 20;
const int M = 4 * 1e5;
int n, maxx, a[M + 5], cnt[N + 5];
LL dp[(1 << N) + 5], cost[N + 5][N + 5];

int main () {
	scanf ("%d", &n);
	for (int i = 1; i <= n; ++ i) {
		scanf ("%d", &a[i]);
		maxx = max (maxx, a[i]);	//maxx不能取成a[i] - 1
		-- a[i];
	}
	for (int i = 1; i <= n; ++ i) {
		++ cnt[a[i]];
		for (int j = 0; j < maxx; ++ j)
			cost[j][a[i]] += cnt[j]; 
	}
	
	dp[0] = 0;
	for (int i = 1; i < (1 << maxx); ++ i)
		dp[i] = INf;
	
	for (int i = 0; i < (1 << maxx); ++ i) {
		for (int j = 0; j < maxx; ++ j) {
			if ((i & (1 << j))) 
				continue;
			LL cost_ = 0;
			for (int k = 0; k < maxx; ++ k)
				if (i & (1 << k))
					cost_ += cost[j][k];
			dp[i + (1 << j)] = min (dp[i + (1 << j)], dp[i] + cost_);
		}
	}
	
	printf ("%lld\n", dp[(1 << maxx) - 1]);
	return 0; 
}

T3.猜数字

题目描述

题目描述
( 256 MiB / 2000 ms )
g老师作为**中学最著名的猜测大师,他对几乎所有的事情都可谓料事如神。这不,g老师又将大展身手,准备进行新一轮事件的猜测。现在,g老师手里有n个数字,他需要猜出一个数字,使得这n个数字都是这个数字除1和它本身外的所有的其他因数。g老师觉得这种问题过于简单,不屑于回答,于是就把这个问题丢给了你们。

输入格式
第一行输入一个数字t(1≤t≤25),表示需要猜测的数字个数。
对于每次猜测,第一行输入一个数字n(1≤n≤300),表示此次猜测的因数个数。
第二行输入n个数字a1,a2,…,an(2≤ai≤10^6),表示需要猜测的数字的第i个因数。题目保证输入的ai各不相同。

输出格式
输出一个数字,即郭老师需要猜测的这个数。如果没有满足题目条件的数字,则输出-1.

样例输入输出
Sample Input
2
8
8 2 12 6 4 24 16 3
1
2

Sample Output
48
4

题解

由于出题人(或者我??)语言功底不佳,所以我搞了半天才搞懂题意,就是水题一道

由于题目保证了a[]是答案除本身和1以外的所有因数,且各不相同,再加上我们知道所有数的因数都是成对出现的,所以直接排序首位乘尾位即可

在计算的过程中,我们可以判断一种输出-1的情况,即是有一对因数的乘积与其他的不同;第二种情况要再计算完之后才可以判断,即是题目给定的a[]数组没有包含完所有的因数,除上述两种情况外,都是合法的

参考代码

#include 
#include 
#include 
using namespace std;
#define LL long long

const int N = 300;
int T, n, a[N + 5];

int main () {
	scanf ("%d", &T);
	while (T --) {
		scanf ("%d", &n);
		for (int i = 1; i <= n; ++ i)	
			scanf ("%d", &a[i]);
		sort (a + 1, a + n + 1);
		
		LL res = 1ll * a[1] * a[n];
		bool flag = true;
		for (int i = 2; i < n; ++ i)
			if (res != 1ll * a[i] * a[n - i + 1]) {
				flag = false;
				break;
			}
		if (! flag) {
			printf ("-1\n");
			continue;
		}
		
		int ip = 1;
		for (LL i = 2; i * i <= res; ++ i) {
			if (res % i == 0) {
				if (a[ip] != i || a[n - ip + 1] != res / i) {
					flag = false;
					break;
				}
				++ ip;
			}
		}
		
		if (! flag)
			printf ("-1\n");
		else
			printf ("%lld\n", res);
	}
	return 0;
}

T4.加分二叉树

题目描述

题目描述

原题来自:NOIP 2003 (512MiB / 1000ms)

设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第j个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数
若某个子树为主,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
(1)tree的最高加分
(2)tree的前序遍历
现在,请你帮助你的好朋友XZ设计一个程序,求得正确的答案。

输入格式
第1行:一个整数n,为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数

输出格式
第1行:一个整数,为最高加分。
第2行:n个用空格隔开的整数,为该树的前序遍历。

数据范围及提示
对于100%的数据,n<30,b<100,结果不超过 4 * 10^9

样例输入输出
Sample Input
5
5 7 1 2 10

Sample Output
145
3 1 2 4 5

题解

前言
首先普及一下二叉树的先序、中序、后序遍历
先序即先根,顾名思义,其遍历顺序为 根–>左儿子–>右儿子
同理,
中序即左儿子–>根–>右儿子
后序即左儿子–>右儿子–>根


正文
下面开始进行该题的讲解

题目给出一颗二叉树的中序遍历,要求前序遍历的最大加分,很显然,我们要做的工作就是,对中序遍历的这棵树进行划分,找出哪些节点是某一棵子树的根,哪些节点是这棵子树的左儿子,哪些节点是这棵子树的右儿子时, 可以得到该子树的最大加分

于是,将题目转化为一道区间dp的板题

根据题目给出的公式(tree = l * r + a),我们设dp[i][j]表示节点i到节点j的最大加分,root[i][j]表示该子树的根节点的编号,于是我们可以得出递推式
d p [ i ] [ j ] = m a x ( d p [ i ] [ k − 1 ] ∗ d p [ i ] [ k + 1 ] + v a l [ k ] ) , k 为 枚 举 的 根 节 点 dp[i][j] = max (dp[i][k - 1] * dp[i][k + 1] + val[k]), k为枚举的根节点 dp[i][j]=max(dp[i][k1]dp[i][k+1]+val[k]),k

答案输出 dp[1][n] 即可

参考代码

(本题解法很多,下面提供一种比较简单的递归算法)

#include 
#include 
using namespace std;

const int N = 30;
int n, val[N + 5];
int dp[N + 5][N + 5], root[N + 5][N + 5];

int dfs (const int l, const int r) {
	if (dp[l][r])
		return dp[l][r];
	if (l == r) {
		dp[l][r] = val[l];
		root[l][r] = l;
		return dp[l][r];
	}
	if (l > r)
		return 1;
	
	for (int i = l; i <= r; ++ i) {
		int t = dfs (l, i - 1) * dfs (i + 1, r) + val[i];
		if (dp[l][r] < t) {
			dp[l][r] = t;
			root[l][r] = i;
		}
	}
	return dp[l][r];
}

void print (const int l, const int r) {
	printf ("%d ", root[l][r]);
	if (root[l][r] > l)
		print (l, root[l][r] - 1);
	if (root[l][r] < r)
		print (root[l][r] + 1, r);
}

int main () {
	scanf ("%d", &n);
	for (int i = 1; i <= n; ++ i)
		scanf ("%d", &val[i]);
	
	dfs (1, n);
	printf ("%d\n", dp[1][n]);
	print (1, n);
	return 0;
}

T5.数字计数

题目描述

题目描述

原题来自:ZJOI 2010 (512MiB / 1000ms)
给定两个正整数a和b,求在[a, b]中的所有整数中,每个数码 (digit) 各出现了多少次。

输入格式
仅包含一行两个整数 a, b,含义如上所述。

输出格式
包含一行10个整数,分别表示0 ~ 9在[a, b]中出现了多少次。

数据范围及提示
30%的数据中,1 ≤ a ≤ b ≤ 10^6,
100%的数据中,1 ≤ a ≤ b ≤ 10^12

样例输入输出
Sample Input
1 99

Sample Output
9 20 20 20 20 20 20 20 20 20

题解

首先,对于30%的数据而言,可以直接暴力枚举每一个数字,然后分离数位计算结果
但是,对于后70%的数据而言,暴力显然是无能为力的,首先10^12就把我吓傻了

冷静下来思考,对于每一道形如求区间[a, b]的某个值的题目,我们都可以很轻松地求地 区间[1, x]或区间[0, x]的值,这个规律在本题也适用,因此,我们可以把区间[a, b]转换成区间[1, b] - 区间[1, a - 1]

显而易见,这道题不是用dp就是用搜索(当然是记忆化),但由于我是个蒟蒻,所以没写出dp,但记忆化确实挺简单的

对于数字i,我们定义dp[len][j][k]表示在长度为len的数字中,i出现j次的情况(其中k表示是否有前导零的)i的个数(有点绕,希望我说清楚了),那么,长度为len的情况只与长度为len - 1的情况有关,因此我们可以采用递归的形式,用记忆化进行优化.

(语文功底很有限啊,口胡只能这样了,代码挺通俗的,慢慢琢磨吧。。。)

对于我上面提到的数位dp的算法,我给出一篇代码通俗易懂的博客,博主文风清奇,忍住别笑

参考代码

#include 
#include 
#include 
using namespace std;
#define LL long long

const int N = 12;
LL cur, t[N + 5], dp[N + 5][N + 5][2];

LL dfs (const int len, const int times, const bool preZero, const bool limit) {
//len表示数字的长度, times表示数字cur出现的次数, preZero表示是否包含前导零, limit表示对首位是否有限制
	if (len == 0)
		return 1ll * times;
	if (! limit && dp[len][times][preZero] != -1)
		return dp[len][times][preZero];
	
	int up = limit ? t[len] : 9;
	LL ans = 0;
	for (int i = 0; i <= up; ++ i) {
		ans += dfs (len - 1, times + ((i || preZero) && (i == cur)), i || preZero, limit && i == t[len]);
	}
	if (! limit)
		dp[len][times][preZero] = ans;
	return ans;
}

LL sovle (LL x) {
	int len = 0;
	while (x) {
		t[++ len] = x % 10;
		x /= 10;
	}
	return dfs (len, 0, 0, 1);
}

int main () {
	LL n, m;
	scanf ("%lld %lld", &n, &m);
	
	for ( ; cur < 10; ++ cur) {
		memset (dp, -1, sizeof (dp));
		LL ans = sovle (m) - sovle (n - 1);
		printf ("%lld ", ans);
	}
	return 0;
}

T6.逛公园

T7.征途

题目描述

题目描述
Pine 开始了从S地到T地的征途。
从S地到T地的路可以划分成n段,相邻两段路的分界点设有休息站。
Pine 计划用m天到达T地。除第m天外,每一天晚上 Pine 都必须在休息站过夜。所以,一段路必须在同一天中走完。
Pine 希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。
帮助 Pine 求出最小方差是多少。
设方差是v,可以证明,v * m²是一个整数。为了避免精度误差,输出结果时输出v * m²。

输入格式
第一行两个数nm。
第二行n个数,表示n段路的长度。

输出格式
一个数,最小方差乘以m²后的值。

样例输入输出
Sample Input
5 2
1 2 5 8 6

Sample Output
36

题解

前言
题目中提到了方差的概念,给大家普及一下:欢迎给度娘增加访问量 :)

方差是实际值与期望值之差平方的平均值
方差是各个数据与平均数之差的平方的和的平均数, 即:
s ² = 1 n [ ( x 1 − x ) 2 + ( x 2 − x ) 2 + . . . + ( x n − x ) 2 ] s² = \frac{1}{n}[(x_1 - x)^2 + (x_2 - x) ^ 2 + ... + (x_n - x) ^2] s²=n1[(x1x)2+(x2x)2+...+(xnx)2]
其中,x表示样本的平均数,n表示样本的数量,xi表示个体,而s2就表示方差。


正文
看到这道题,我们首先要知道我们要求什么。根据题意,我们最后需要输出的是v * m²,其中v是方差

显然,这道题是要用dp的。那么,我们设第i天的路程为ai,那么 v = ∑ ( a i − ( S m ) 2 m v= \frac{∑(a_i - (\frac{S}{m})^2}{m} v=m(ai(mS)2,代入所求式子中,化简可得: a n s = m ∗ ∑ a i 2 − S 2 ans = m * ∑a_i^2-S^2 ans=mai2S2

进一步思考,由于m和s都是已知的,所以只需让 ∑ a i ² ∑a_i² ai²最小即可。
要使得和最小,我们可以设dp[i][j]表示前i天走了前j段路的最优解(前i天所走路程的平方和),那么很容易可以想到状态转移方程式:
d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ k ] + ( p r e [ j ] − p r e [ k ] ) 2 ) dp[i][j] = min (dp[i - 1][k] + (pre[j] - pre[k])^2) dp[i][j]=min(dp[i1][k]+(pre[j]pre[k])2),其中pre[i]表示前i段路的长度和,1 ≤ k<j。利用这个状态转移方程,我们可以很容易地对答案进行dp求解,时间复杂度为 O ( m 3 ) O(m^3) O(m3),对于100%的数据,显然是拿不全的


接下来,我们开始思考优化,对于这个式子,它的第一维(即i)是可以滚动的,因此,我们考虑斜率优化
对于节点i,我们考虑a和b两种情况。若a比b优,那么有:
d p [ j − 1 ] [ a ] + ( p r e i − p r e a ) 2 < d p [ j − 1 ] [ b ] + ( p r e i − p r e b ) 2 dp[j - 1][a] + (pre_i - pre_a) ^ 2 < dp[j - 1][b] + (pre_i - pre_b) ^ 2 dp[j1][a]+(preiprea)2<dp[j1][b]+(preipreb)2
用平方差公式展开,化简得:
d p [ j − 1 ] [ a ] + p r e a ² − 2 ∗ p r e i ∗ p r e a < d p [ j − 1 ] [ b ] + p r e b ² − 2 ∗ p r e i ∗ p r e b dp[j - 1][a] + pre_a² - 2 * pre_i * pre_ a < dp[j - 1][b] + pre_b² - 2 * pre_i * pre _ b dp[j1][a]+prea²2preiprea<dp[j1][b]+preb²2preipreb
进一步变形,得:
( d p [ j − 1 ] [ a ] + p r e s a ² ) − ( d p [ j − 1 ] [ b ] + p r e b ² ) p r e a − p r e b < 2 ∗ p r e i \frac{(dp[j - 1][a] + pres_a²) - (dp[j - 1][b] + pre_b²)}{pre_a - pre_b} < 2 * pre_i preapreb(dp[j1][a]+presa²)(dp[j1][b]+preb²)<2prei
化简到此,我们可以发现,不等式左边其实就是一个斜率的式子,然后直接套上斜率优化的模板就可以了

参考代码

(未加斜率优化,60分暴力dp)

#include 
#include 
#include 
#include 
using namespace std;

const int N = 3000;
int n, m, a[N + 5], pre[N + 5];
int dp[N + 5][N + 5];

int main () {
	scanf ("%d %d", &n, &m);
	for (int i = 1; i <= n; ++ i) {
		scanf ("%d", &a[i]);
		pre[i] = pre[i - 1] + a[i];
	}
	
	memset (dp, 0x3f, sizeof (dp));
	dp[0][0] = 0;
	for (int i = 1; i <= m; ++ i)
		for (int j = 1; j <= n; ++ j)
			for (int k = 0; k < j; ++ k)
				dp[i][j] = min ( dp[i][j], dp[i - 1][k] + (pre[j] - pre[k]) * (pre[j] - pre[k]) );
	
	printf ("%lld\n", m * dp[m][n] - pre[n] * pre[n]);
	return 0;
} 

(加上斜率优化后开心地Ac了 )

#include 
#include 
#include 
#include 
using namespace std;

const int N = 3000;
int n, m, a[N + 5], pre[N + 5];
int dp[N + 5][N + 5], deque[N + 5];

double count (const int i, const int j, const int k) {
	double t = dp[i][j] + pre[j] * pre[j] - dp[i][k] - pre[k] * pre[k];
	return t / (pre[j] - pre[k]);
}
//注意整数除以整数会被强制取整
//如果写成除法怕精度出错的话,可以移项成乘法来计算,但是我懒。。。

int main () {
	scanf ("%d %d", &n, &m);
	for (int i = 1; i <= n; ++ i) {
		scanf ("%d", &a[i]);
		pre[i] = pre[i - 1] + a[i];
	}
	
	for (int i = 1; i <= n; ++ i)
		dp[1][i] = pre[i] * pre[i];
		
	for (int i = 2; i <= m; ++ i) {
		int head = 1, tail = 0;
		for (int j = 1; j <= n; ++ j) {
			while (head < tail && count (i - 1, deque[head], deque[head + 1]) < 2 * pre[j])
				++ head;
			
			int k = deque[head];
			dp[i][j] = dp[i - 1][k] + (pre[k] - pre[j]) * (pre[k] - pre[j]);
			
			while (head < tail && count (i - 1, deque[tail - 1], deque[tail]) > count (i - 1, deque[tail], j))
				-- tail;
			deque[++ tail] = j;
		}
	}
	
	printf ("%d\n", m * dp[m][n] - pre[n] * pre[n]);
	return 0;
}

T8.股票交易

T9.麻将

T10.太空梯

题目描述

题目描述
(256 MiB / 1000 ms)
有一群牛要上太空。他们计划建一个太空梯-----用一些石头垒。他们有k(1<=k<=400)种不同类型的石头,每一种石头的高度为h_i(1<=hi<=100),数量为ci(1<=ci<=10),并且由于会受到太空辐射,每一种石头不能超过这种石头的最大建造高度ai(1<=ai<=40000)。帮助这群牛建造一个最高的太空梯。

输入格式
第一行为一个整数即k。第2行到第k+1行每一行有3个数,代表每种类型魔法石的特征,即高度h,限制高度a和数量c。

输出格式
一个整数,即修建太空梯的最大高度。

样例输入输出
Sample Input
3
7 40 3
5 23 8
2 52 6

Sample Output
48

样例说明
15+21+12
最底下为3块石头2型,中间为3块石头1型,上面为6块石头3型。放置4块石头2型和3块石头1型是不可以的,因为顶端的石头1型的高度超过了40的限制

题解

这是一道比较裸的多重背包问题,由于题目比较简单,也可以转化成01背包来做
首先,由于题目对石头的安装高度有限制,所以我们首先要对石头的高度限制进行排序,然后再套背包的模板就可以啦

参考代码

(本题解法很多,下面给出转化成01背包的做法之一)

#include 
#include 
#include 
using namespace std;

const int N = 400;
int n;
bool dp[40005];
struct node {
	int hight, limit, cnt;
	node () {}
	node (const int Hight, const int Limit, const int Cnt) {
		hight = Hight;
		limit = Limit;
		cnt = Cnt;
	}
	
	bool operator < (const node tmp) const{
		return limit> tmp.limit;
	}
}stone[N + 5];

int main () {
	scanf ("%d", &n);
	for (int i = 1; i <= n; ++ i) {
		int x, y, z;
		scanf ("%d %d %d", &x, &y, &z);
		stone[i] = node (x, y, z);
	}
	
	sort (stone + 1, stone + n + 1);
	
	dp[0] = true;
	for (int i = 1; i <= n; ++ i)
		for (int j = 1; j <= stone[i].cnt; ++ j)
			for (int k = stone[i].limit; k >= stone[i].hight; -- k)
				dp[k] = dp[k] | dp[k - stone[i].hight];
	
	int ans = 0;
	for (int i = stone[n].limit; i >= 0; -- i)
		if (dp[i]) {
			ans = i;
			break;
		}
	
	printf ("%d\n", ans);
	return 0;
}

后记

dp是每一年noip提高组都会考的,有一点拉分的性质,每次都是卡我的题,也是很多人比较弱的板块,只能是多练练啦,混个脸熟就好了。

你可能感兴趣的:([2019年国庆专题训练] dp专题训练)