前篇传送门:
动态规划问题(一)_Wmiracle的博客-CSDN博客
动态规划问题(二)_Wmiracle的博客-CSDN博客
有 n 堆石子排成一排,每堆石子有一定的数量,将 n 堆石子合并成一堆。合并的规则是每次只能合并相邻的两堆石子,合并的花费为这两堆石子的总数。石子经过 n - 1 次合并后成为一堆,求总的最小花费。
以最小花费为例,设表示合并第 i 堆石子到第 j 堆石子的最小花费,为从第 i 到 j 的区间的石子数和(即花费和)。
我们从最简单的合并——两堆合并开始分析:合并第 i 堆和第 i + 1 堆的花费为第 i 堆的花费 + 第 i + 1 堆的花费,考虑到第 i 堆和第 i + 1 堆可能分别已经发生过合并,因此可以得到;
再分析三堆合并:合并第 i 堆、第 i + 1 堆和第 i + 2堆的花费有两种情况,
一种是先合并第 i 堆和第 i + 1堆,再和第 i + 2堆合并,这种情况下;
另一种是先合并第 i + 1堆和第 i + 2堆,再和第 i 堆合并,这种情况下。
再推广到第 i 堆到第 j 堆的合并,可以进一步化为第 i 到第 k 堆的合并 + 第 k + 1 到第 j 堆的合并,因此状态转移方程为
C++未优化部分代码如下:
for(int i = 1;i <= n; i++)
dp[i][i] = 0;
for(int len = 1; len < n; len++){
for(int i = 1; i <= n - len; i++){
int j = i + len;
dp[i][j] = INF;
for(int k = i; k < j; k++)
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
}
}
很明显时间复杂度为,只适用于小数据的情况。当数据较大时,我们可以使用平行四边形优化的方法。用表示从第 i 堆石子到第 j 堆石子的最优分割点,由平行四边形优化原理,可以得到,那么 k 只需枚举到,每次记录最优分割点即可。
C++优化部分代码如下:
for(int i = 1;i <= n; i++){
dp[i][i] = 0;
s[i][i] = i;
}
for(int len = 1; len < n; len++){
for(int i = 1; i <= n - len; i++){
int j = i + len;
dp[i][j] = INF;
for(int k = s[i][j - 1]; k <= s[i + 1][j]; k++)
if(dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1] < dp[i][j]){
dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
s[i][j] = k;
}
}
}
例题:AcWing282 石子合并
某大学有 n 个职员,编号为 1...n。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 ,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
我们令表示不选择当前结点的最优解,表示选择当前结点的最优解,可以分成两种情况:
第一种是不选择当前结点,那么子结点的选择没有限制,即既可选择子结点,又可不选择子结点,因此只要取两者的最大值,即;
第二种是选择当前结点,那么子结点只能不选择,即。
综上分析,状态转移方程为,其中为根结点。
C++代码如下:
#include
#include
#include
#include
using namespace std;
const int N = 6000 + 5;
int value[N], dp[N][2], father[N]; // value[i]存储结点i的权值,father[i]存储结点i的父结点
vector tree[N];
void dfs(int u){
dp[u][0] = 0; // u不参加宴会的初值
dp[u][1] = value[u]; // u参加宴会的初值
for(int i = 0; i < tree[u].size(); i++){ // 处理u的子结点
int son = tree[u][i];
dfs(son);
dp[u][0] += max(dp[son][1], dp[son][0]); // 第一种情况
dp[u][1] += dp[son][0]; // 第二种情况
}
}
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> value[i];
father[i] = -1; //赋初值
}
for(int i = 1; i <= n - 1; i++){
int a, b;
cin >> a >> b;
tree[b].push_back(a); // 邻接表建树
father[a] = b; // 建立父子关系
}
int t = 1;
while(father[t] != -1){ // 查找根结点
t = father[t];
}
dfs(t); // dfs遍历整棵树
cout << max(dp[t][1], dp[t][0]) << endl;
return 0;
}
例题:luoguP1352 没有上司的舞会
一个数字,如果包含'4'或者'62',它是不吉利的。给定m和n,统计[m, n]范围内吉利数的个数。
可以从高位到低位的顺序进行排除不符合条件的数,例如范围给定[1, 999999],先排除最高位是4的数和最高位是6,次高位是2的数,即400000~499999、620000~629999;在剩下的数中再排除次高位是4的数和次高位是6,次次高位是2的数;以此类推直到结束。实现这个思路有两种方法,一种是递推,另一种是记忆化搜索。
先来看递推实现数位DP:
令表示 i 位数中首位是 j 符合要求的数的个数。非常容易得到状态转移方程为
C++代码如下:
#include
#include
#include
using namespace std;
const int LEN = 7 + 1;
int dp[LEN][10];
int digit[LEN]; // digit[i]存储第i位数字
void init(){ // 预处理计算dp[][]
dp[0][0] = 1;
for(int i = 1; i < LEN; i++)
for(int j = 0; j < 10; j++){
if(j != 4) // 排除数字4
for(int k = 0; k < 10; k++){
if(j == 6 && k == 2) continue; // 排除数字62
dp[i][j] += dp[i - 1][k];
}
}
}
int solve(int n){
int len = 0; // len表示n的位数
while(n){
digit[++len] = n % 10;
n /= 10;
}
digit[len + 1] = 0;
int ans = 0;
for(int i = len; i > 0; i--){ // 从高位到低位处理
for(int j = 0; j < digit[i]; j++){
if(j == 2 && digit[i + 1] == 6) continue;
ans += dp[i][j];
}
if(digit[i] == 4 || digit[i] == 2 && digit[i + 1] == 6) break; // 第i位是4或者第i+1位和第i位是62不符合条件
}
return ans;
}
int main()
{
int n, m;
init();
while(cin >> n >> m && (n | m)){
cout << solve(m + 1) - solve(n) << endl;
}
return 0;
}
再看看用记忆化搜素的方法实现数位DP:
令表示不含4和62的前提下首位是否为6的数的个数(1表示是,2表示否),记忆化搜索的本质在于用dfs遍历到最深处(本题以数位长度不断减少1来遍历,最深处为数位长度等于0),然后逐步回退,将每次计算结果保存下来,在再次遇见相同的计算时可以直接使用。
C++代码如下:
#include
#include
#include
using namespace std;
const int LEN = 7 + 1;
int dp[LEN][2]; // dp[i][0 / 1]表示不含4和62的前提下首位是否为6的个数(1表示是,0表示否)
int digit[LEN];
int dfs(int len, bool state, bool fp){ // state表示dp的状态,即不含4和62的前提下首位是否为6
if(!len) return 1; // 已经递归到0位数,返回
if(!fp && dp[len][state] != -1) return dp[len][state]; // 如果已经算过则直接使用
int res = 0, fpmax = fp ? digit[len] : 9;
for(int i = 0; i <= fpmax; i++){
if(i == 4 || state && i == 2) continue; // 排除出现4和62的情况
res += dfs(len - 1, i == 6, fp && i == fpmax);
}
if(!fp) dp[len][state] = res;
return res;
}
int solve(int n){
int len = 0;
while(n){
digit[++len] = n % 10;
n /= 10;
}
return dfs(len, false, true);
}
int main()
{
int n, m;
memset(dp, -1, sizeof dp); // 初始化为-1
while(cin >> n >> m && (n | m)){
cout << solve(m) - solve(n - 1) << endl;
}
return 0;
}
例题:hdu2089 不要62
农夫约翰有一片长方形土地,划成 M 行 N 列的方格。他准备种玉米、养牛。
遗憾的是,有些田地很贫瘠,不能种玉米。
而且,牛不喜欢彼此靠近吃东西,所以牛不能放在相邻的格子里。
给出这块地的情况,求约翰有多少种种玉米的方案。
所有方格不种玉米也算一种方案。
既然是个方格图,首先得对这些方格进行表示,由于方格上只有两种状态:种玉米和不种玉米,因此容易想到可以用二进制来表示,用1表示种玉米,0表示不种玉米。以每一行为一个二进制序列,每种二进制序列用一个编号表示,如1表示000,2表示001,3表示010,4表示100(排除了相邻情况)……。我们可以令表示第 i 行采用第 j 种编号的方案时前 i 行可以得到的可行方案总数。
从第 i - 1 行转移到第 i 行,第 i 行取每种方案的可行方案总数为,其中得保证所有的与第 i 行不冲突。
最后把最后一行的相加即为答案。
注意:相邻两行是否有挨着的1可以这样判断:
if(state[i] & state[j]) {...} // 相邻两行有挨着的1
if(!(state[i] & state[j])) {...} // 相邻两行没有挨着的1
C++代码如下:
#include
#include
#include
using namespace std;
typedef long long ll;
const int mod = 1000000000;
const int N = 1 << (12 + 5);
int n, m, cnt;
int state[N], initst[N]; // state[i]存储一行中第i种可行状态,initst[]存储原状态
int dp[12 + 5][N];
bool check(int state){ // 判断是否有相邻的
if(state & (state << 1)) return false; // 有相邻的1,不合法
return true; // 没有相邻的1,合法
}
int solve(int n, int m)
{
for(int i = 0; i < (1 << m); i++) // 初始化合法方案
if(check(i)) state[cnt++] = i; // 记录合法方案
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++){
int x;
cin >> x;
if(x == 0) initst[i] |= (1 << j);
}
for(int i = 0; i < cnt; i++)
if(!(initst[0] & state[i])) dp[0][i] = 1; // 将可行状态且不与第一行发生冲突的初始化为1
for(int i = 1; i < n; i++)
for(int j = 0; j < cnt; j++){
if(initst[i] & state[j]) continue; // 有挨着的1,不合法
for(int k = 0; k < cnt; k++){
if(initst[i - 1] & state[k] || state[j] & state[k]) continue; // 有挨着的1,不合法
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;
}
}
int res = 0;
for(int i = 0; i < cnt; i++)
res = (res + dp[n - 1][i]) % mod;
return res;
}
int main()
{
cin >> n >> m;
cout << solve(n, m) << endl;
return 0;
}
例题:poj3254 Corn Fields(玉米田)
luoguP2704 炮兵阵地