dp做题笔记3

这次的题主要是划分型的dp,即将一个序列或者字符串划分成若干个满足要求的段。做法是考虑最后一段,枚举最后一段的起点。下面直接上题目啦~

1.给定一个正整数n,问最少可以将n分成几个完全平方数之和,比如 13 = 2*2 + 3*3 答案为2

分析:

先确定状态,对于最后一步,关注最优策略中的最后一个完全平方数j²,那么最优策略中n-j²也一定被划分成最少的完全平方数之和,我们设f[i]表示i最少被分成几个完全平方数之和,那么不难得到

f[i] = min{f[i - j²] + 1} 1<= j * j <=I

初始条件f[0] = 0 ,那么 ans = f[n]

#include<bits/stdc++.h>

using namespace std;

const int INF = 0x3f3f3f3f;

int Dp(int n) {
	int f[n + 1];
	f[0] = 0;
	for (int i = 1; i <= n; ++i) {
		f[i] = INF;
		for (int j = 1; j * j <= i; ++j) {
			if (f[i - j * j] + 1 <  f[i])
				f[i] = f[i - j * j] + 1;
		}
	}
	return f[n];
}

int main()
{
	int n; cin >> n;
	cout << Dp(n) << endl;



	return 0;
}

2.给定一个字符串S[0…N-1],现要将其划分成若干段,每一段都是回文串,求最少划分次数,例 S = "aab" 划分一次即可 aa,b

分析:

最后一步:关注最优策略中的最后一段回文串,设为S[j…N-1]

需要知道S前j个字符[0…j-1]最少可以划分成几个回文串

我们不妨设S前i个字符S[0…N-1]最少可以划分成f[i]个回文串,我们可以得到状态转移方程:

f[i] = min{f[j] + 1} S[j…i-1]是回文串且 j < I

初始条件f[0] = 0

还剩下最后一个问题,如何判断回文串?我们可以用两个指针,一个头指针,一个尾指针 向中间夹,但是这样做法太慢了。回文串分两种,一种长度为奇数,一种长度为偶数。(这不废话吗.)

奇数回文串的特点,比如aba,偶数回文串一定是两边

我们反过来思考,如果让你生成一个回文串你会怎么做,肯定是从中间开始噻,从中间向两边扩展每次左右两端加上同样的字符即可。

所以我们以字符串的每一个字符为中点,向两边扩展找到所有的回文串,其次我们还需要记录回文串

注意考虑奇数回文串和偶数回文串,用isHuiwen[i][j]表示S[i…j]是否是回文串

最终答案为f[N] - 1  因为本身不算所以要减一

#include<bits/stdc++.h>

using namespace std;
const int INF = 0x3f3f3f3f;

int Dp(string s) {
	int n = s.length();
	if (n == 0) return 0;
	bool isHuiwen[n][n];
	//memset(isHuiwen, false, sizeof(isHuiwen));
	int i, j, t;
	for (i = 0; i < n; ++i) {
		for (j = i; j < n; ++j) {
			isHuiwen[i][j] = false;
		}
	}
	//以t为中心点找回文串
	for (t = 0; t < n; ++t) {
		i = j = t;
		//回文串长度为奇数
		while (i >= 0 && j < n && s[i] == s[j]) {
			isHuiwen[i][j] = true;
			--i;
			++j;
		}
		//回文串长度为偶数
		i = t;
		j = t + 1;
		while (i >= 0 && j < n && s[i] == s[j]) {
			isHuiwen[i][j] = true;
			--i;
			++j;
		}
	}
	int f[n + 1];
	f[0] = 0;
	for (i = 1; i <= n; ++i) {
		f[i] = INF;
		for (j = 0; j < i; ++j) {
			if (isHuiwen[j][i - 1]) {
				f[i] = min(f[j] + 1, f[i]);
			}
		}
	}
	return f[n] - 1;
}

int main()
{
	string s; cin >> s;
	cout << Dp(s) << endl;




	return 0;
}

3.有N本书需要被抄写,第i本书有A[i]页,i=0,1,2,…,N-1

有K个抄写员,每个抄写员可以抄连续的若干本书,每个抄写员的速度都相同,均为一分钟一页,问最少需要多少时间抄写完所有的书。请注意,多个人不能抄同一本!

输入: n = 3,A= [3, 2, 4]  k = 2

输出:5 (第一个抄写员抄第一本和第二本,第二个抄写员抄第三本)

分析:

如果一个抄写员从第i本书抄到第j本书,则需要时间A[i] + A[i+1] + … + A[j]

最后的完成时间取决于耗时最长的那个抄写员

我们可以将题意转换一下,将一系列数字分成不超过K组,使得所有段的数字之和的最大值最小

  • 最后一步:最优策略中最后一个抄写员(就叫他Bob吧,设他是第K个)抄写的部分--一段连续的书,包括最后一本
  • 如果Bob抄写第j本到第N-1本书
  • 则Bob需要的时间为 A[j] + A[j + 1] + … + A[N-1]
  • 我们还需要知道前面K - 1个人最少需要多少时间抄完前j本书(0 ~ j -  1)

我们设 f[k][i] 为k个抄写员最少需要多少时间抄完前i本书,则

  • 初始条件

--0个抄写员只能抄0本书,故f[0][0] = … = f[0][N] = +∞

--k个抄写员(k > 0)需要0时间抄0本书,即 f[k][0] = 0

  • 如果K > N 可以令K = N,这个时候一人一本取最大值即可(两个人不能抄同一本)

#include<bits/stdc++.h>

using namespace std;
const int INF = 0x3f3f3f3f;

int n, k;

int Dp(int A[], int K) {
	if (n == 0) return 0;
	if (K > n) K = n;
	int f[n + 1][n + 1];
	memset(f,0,sizeof(f));
	int i, j, k;
	for (j = 1; j <= n; ++j) {
		f[0][j] = INF;
	}
	int sum = 0;
	for (k = 1; k <= K; ++k) {
		f[k][0] = 0;
		for (i = 1; i <= n; ++i) {
			f[k][i] = INF;
			sum = 0;
			for (j = i; j >=0; --j) {
				f[k][i] = min(f[k][i],max(f[k - 1][j],sum));
				if (j > 0) sum += A[j-1];
			}
		}
	}
	return f[K][n];
}

int main()
{
	cin >> n >> k;
	int A[n];
	for (int i = 0; i < n; ++i) cin>>A[i];
	cout << Dp(A, k) << endl;




	return 0;
}

4.Bash博弈

有一排N个石子,Alice和Bob轮流取石子,每次一个人可以从最右边取走一个或两个石子,取走最后一个石子的人获胜,问先手Alice是否必胜。例如 N = 5,Alice先手必胜,必胜策略为先手取2个石子,无论后手拿几个石子,Alice都可以将最后的几块石子一次性拿完

分析:

博弈型的dp通常从第一步开始分析,而不是最后一步。因为局面越来越简单,石子数越来越少

面对N个石子,先手Alice可以拿1个或者两个石子,这样后手Bob就面对N-1个石子或N-2个石子的局面

Alice一定会选择能让自己赢的策略,毕竟双方都采取最优策略嘛~

假设Bob后手面对N-1个石子,这其实和Bob一开始是先手,有N-1个石子的情况是一样的,那么此时Bob也会选择让自己赢的策略,取走1个或2个石子,之后Alice面对新的局面,自己成为新的先手,选择让自己赢的策略

那么这个时候问题就来了,如何选择让自己赢的一步?

就是走了这一步后,对手面对剩下的石子,他处于必败态,比如五个石子:

可以知道是,如果取走1个或2个石子后,能够让剩下的局面为先手必败,则当前先手必胜

如果不管怎么取,剩下的局面都是先手必胜,则当前先手必败

通过以上分析,可以得到

  • 需要知道面对N-1或N-2个石子,先手是否必胜
  • 子问题
  • 状态:设f[i]表示面对i个石子,是否先手必胜

不难知道f[i]的状态转移方程

 

 

可以发现f[i] = f[i - 1] == False OR f[i - 2] ==false 则必胜,因为下一次先手必败

注意 f[0] = 0,0个石子,先手必败,f[1] = f[2] = true 能直接拿完,先手必胜

#include<bits/stdc++.h>

using namespace std;

bool Dp(int n) {
	if (n == 0) return false;
	if (n <= 2) return true;
	bool f[n + 1];
	memset(f, false, sizeof(f));
	f[0] = false;
	f[1] = f[2] = true;
	for (int i = 3; i <= n; ++i) {
		f[i] = (f[i-1] == false) || (f[i-2] == false);
	}
	return f[n];
}

int main()
{
	int n; cin >> n;
	if (Dp(n)) cout << "true" << endl;
	else cout << "false" << endl;
	return 0;
}

其实对于这题,还有更简单的代码实现,只不过既然当做dp来做姑且就先委屈一下啦~

其实只要 n mod 3 = 0 则后手胜,否则先手胜

此题是简化版的bash博弈,一般化的bash博弈为

有一堆总数为n的物品,2名玩家轮流从中拿取物品。每次至少拿1件,至多拿m件,不能不拿,最终将物品拿完者获胜。

在先取完者胜的巴什博弈中,若n可被m+1整除,则后手方必胜,否则先手方必胜。具体策略分析如下:

如果n=(m+1)r+s,(r为任意自然数,s≤m),那么先取者要拿走s个物品,如果后取者拿走k(≤m)个,那么先取者再拿走m+1-k个,结果剩下(m+1)(r-1)个,以后保持这样的取法,那么先取者肯定获胜总之,要保持给对手留下(m+1)的倍数,就能最后获胜

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只微

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值