这次的题主要是划分型的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)的倍数,就能最后获胜