DP 总结

DP,即动态规划(Dynamic Programming),是数学中比较冷门的学科,但在信息学中的用处很大。主要有(基础):

  • 背包问题
  • 区间 DP
  • 状态压缩 DP
  • 树形 DP

更深入的有:

  • 插头 DP
  • 概率 DP

DP 特征:无后效性,最优子结构,子问题重叠。

  • 无后效性:也就是当前决策完后,后面的决策不再受当前的决策的影响。

  • 最优子结构:最优解的子问题也是最优的

  • 子问题重叠:一个决策不会是独立的,可能在后面的决策中用到。

背包问题:

0-1 背包

例题:luogu P1048 采药

  • 题意:

给出物品的重量 \(w_i\) 和价值 \(v_i\),每个物品只能选一次,问最大价值。

  • 思路:

设方程 \(f_{i,j}\) 为取前 \(i\) 个物品,容量为 \(j\) 时的最大价值。
\(f_{i,j}=\max\{f_{i-1,j}, f_{i-1,j-w_i}+v_i\}\)。什么意思呢:如果当前物品不选,那么直接从 \(i-1\) 转移过来(\(f_{i-1,j}\))。如果当前选,那么这个除了物品之外的总容量为 \(j - w_i\),那我们直接调用 \(f_{i-1,j-w_i}\) 加上选的价值即可。

点击查看代码
for (int i = 1; i <= m; i++) {
  for (int j = t; j >= w[i]; j--) {
      dp[j] = max(dp[j], dp[j - w[i]] + e[i]);
  }
}

完全背包

例题:luogu P1616 疯狂的采药

  • 题意:

给出物品的重量 \(w_i\) 和价值 \(v_i\),每个物品可以选无限次,问最大价值。

  • 思路:

考虑 \(O(n^3)\) 暴力,枚举 \(k\) 为选择物品的数量(\(k\le \dfrac{w_i}{j}\)),所以转移方程为:

\(f_{i,j}=\max\{f_{i-1,j\times k}+v_i\times k\}\)

怎么优化呢?

发现对于 \(f_{i,j-w_i}\)\(f_{i,j-w_i\times k}\) 转移过来,所以我们就不用枚举 \(k\) 了,即 \(f_{i,j}\)\(f_{i,j-w_i}\) 转移过来。

然后可以滚动数组优化掉第一维:

点击查看代码
for (long long i = 1; i <= m; i++) {
  for (long long j = e[i]; j <= t; j++) {
    dp[j] = max(dp[j], dp[j - e[i]] + w[i]);
  }
}

多重背包

其实就朴素的多重背包。

转移方程:\(f_{i,j}=\max\limits_{k=0}^nk_i\{f_{i-1,j-w_i \times k}+v_i\times k\}\)

混合背包

就是上面三种背包的结合。判断一下做出相应匹配背包即可。

分组背包

例题:luogu P1757 通天之分组背包

  • 题意:

\(n\) 件物品和一个大小为 \(m\) 的背包,第 \(i\) 个物品的价值为 \(w_i\),体积为 \(v_i\)每个物品属于一个组,同组内最多只能选择一个物品。求背包能装载物品的最大总价值。

  • 思路:

就是对于每组求一次 0-1 背包即可。

点击查看代码
for (int k = 1; k <= ts; k++) {
  for (int i = m; i >= 0; i--) {
    for (int j = 1; j <= cnt[k]; j++) {
      if (i >= w[t[k][j]]) {
        dp[i] = max(dp[i], dp[i - w[t[k][j]]] + c[t[k][j]]);
	  }
	}
  }
}

区间 DP

顾名思义,就是在区间上进行 DP。

DP 顺序为从区间长度小的转移到大的上来。所以我们可以枚举长度 \(k\),然后枚举两个端点 \(i,j\)\(j\)\(i+k-1\)。然后我们就在这个区间 \([i,j]\) 做操作。

例题:luogu P1063 能量项链

  • 题意:

有一个环,每次可以选择相邻两个合并,如果前一颗能量珠的头标记为 \(m\),尾标记为 \(r\),后一颗能量珠的头标记为 \(r\),尾标记为 \(n\),则聚合后释放的能量为 \(m\times r\times n\),头标为 \(m\),尾标为 \(n\)。求最大代价。

  • 思路:

首先破环为链。

显然 \(f_{i,j}=\max\{f_{i,j}, f{i,k}+f_{k+1,j}+a_i \times a_{k+1} \times a_{j+1}\}\)

就是由区间 \([i,k]\) 和区间 \([k+1,j]\) 的最大值相加得到的。然后还要加上珠子合并所产生的代价。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int MAXN = 1e2 + 100;

int n, a[MAXN], dp[MAXN][MAXN], ans;

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    a[i + n] = a[i];
  }
  for (int l = 2; l <= n; l++) {
    for (int i = 1; i + l - 1 <= n << 1; i++) {
      int j = i + l - 1;
      for (int k = i; k < j; k++) {
        ans = max(ans, dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + a[i] * a[k + 1] * a[j + 1]));
      }
    }
  }
  cout << ans;
  return 0;
}

状压 DP

也就是状态压缩 DP,具体的,我们把 \(x\) 拆分成二进制形式把他存到 \(dp\) 数组里进行处理,所以题目给出的 \(n\) 必须很小,一般是在 \(21\) 以内。

Tips:

  • 判断一个数字 \(x\) 进制下第 \(y\) 位是不是等于 \(1\)

方法:if(((1 << (y − 1)) & x))

  • 将一个数字 \(x\) 二进制下第 \(y\) 位更改成 \(1\)

方法:x = x | (1 << (y − 1))

  • 将一个数字 \(x\) 二进制下第 \(y\) 位更改成 \(0\)

方法:x = x & ~(1 << (y − 1))

例题:luogu P1433 吃奶酪

显然我们先要预处理出每一个点间的距离,这个可以用 \(O(n^2)\) 暴力实现,然后考虑怎么 DP。

我们设 \(dp_{i,j}\) 表示走到 \(i\),经过路径的二进制为 \(j\) 的最短距离,显然初始化是 \(dp_{i,2^{i-1}}=a_{0,i}\)

然后我们暴力枚举所有的二进制形式 \(k\),然后我们枚举当前可以到达的点 \(i\),也就是满足 \(k \& 2^{i-1} \neq 0\),即 \(i\) 这个点在 \(k\) 中做过没有,然后再去枚举所有可以从 \(i\) 出发到达的点 \(j\)\(j\) 要满足 \(i \neq j\) 并且 \(k \& 2^{j-1} \neq 0\)。那我们就可以得到转移方程:\(dp_{i,k}=\min\{dp_{i,k}, dp_{j, k - 2^{i - 1}} + dis_{i,j}\}\)

答案:\(\min\limits_{i=1}^n dp_{i,2^n-1}\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

int t, n, m;
double x[16], y[16], f[16][16], dp[16][1 << 16];

double Manhattan(double x1, double y_1, double x2, double y2) {
  return sqrt(1.0 * (x1 - x2) * (x1 - x2) + 1.0 * (y_1 - y2) * (y_1 - y2));
}

int main() {
  cin >> n;
  memset(dp, 127, sizeof(dp));
  for (int i = 1; i <= n; i++) {
    cin >> x[i] >> y[i];
  }
  x[0] = y[0] = 0;
  for (int i = 0; i <= n; i++) {
    for (int j = i + 1; j <= n; j++) {
      f[i][j] = f[j][i] = Manhattan(x[i], y[i], x[j], y[j]);
    }
  }
  for (int i = 1; i <= n; i++) {
    dp[i][1 << (i - 1)] = f[0][i];
  }
  for (int S = 1; S < (1 << n); S++) {
  	for (int i = 1; i <= n; i++) {
  	  if ((S & (1 << (i - 1))) == 0) {
		continue;
	  }
  	  for (int j = 1; j <= n; j++) {
  	    if (i == j)  {					
          continue;
		}
		if ((S & (1 << (j - 1))) == 0) { 
		  continue;
		} 
  	    dp[i][S] = min(dp[i][S], dp[j][S - (1 << (i - 1))] + f[i][j]);
      }
    }
  }
  double maxx = 10000000.0;
  for (int i = 1; i <= n; i++) {
    maxx = min(maxx, dp[i][(1 << n) - 1]); 
  }
  cout << fixed << setprecision(2) << maxx << '\n';	
  return 0;
} 

树形 DP

普通的树形 DP

顾名思义,就是在树上进行 DP,一般和 dfs 一起出现。

这类问题我们优先考虑以当前节点 \(x\) 为根的贡献是多少再去转移。

例题:luogu P1352 没有上司的舞会

\(dp_{x,0/1}\) 如果 \(x\) 去或者不去的最优答案,那么如果 \(x\) 去,那么 \(x\) 的下属都不会去,也就是 \(dp_{x,1}=\sum dp_{y,0}+a_x\)(其中 \(y\)\(x\) 的下属)。那么如果 \(x\) 不去,那么 \(x\) 的下属都会去,即 \(dp_{x,0}=\sum \max\{dp_{y,0}, dp_{y,1}\}\)

最后答案就是 \(\max\{dp_{n,0}, dp_{n,1}\}\)

点击查看代码
#include <bits/stdc++.h>

using namespace std;

vector<int> son[1000001];

int n, dp[100001][2], flag[100001], r[100001];

void dfs(int u, int fa) {
  dp[u][0] = 0;
  for (int i = 0; i < son[u].size(); i++) {
    int v = son[u][i];
    dfs(v, u);
    dp[u][0] += max(dp[v][1], dp[v][0]);
    dp[u][1] += dp[v][0];
  }
}

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> r[i];
    dp[i][1] = r[i];
  }
  for (int i = 1; i < n; i++) {
    int l, k;
    cin >> l >> k;
    son[k].push_back(l); 
    flag[l] = 1;
  } 
  for (int i = 1; i <= n; i++) {
    if (!flag[i]) {
      dfs(i, 0);
      cout << max(dp[i][0], dp[i][1]); 
      return 0;
    }
  }
  return 0;
}

换根 DP

换根 DP 也叫二次扫描法,是特殊的一种树形 DP。

分为几个步骤:

  1. 从节点 \(x\) 开始 dfs 得到以 \(x\) 为根整个树的答案

  2. 换根,试将根换成与 \(x\) 有边相连的点统计最优答案。

例题:luogu P3478 [POI2008] STA-Station

显然我们可以枚举每个 \(i\) 去 dfs 出以 \(i\) 为根的树的答案,再去统计最大值,但是显然时间复杂度 \(O(n^2)\)\(\texttt{TLE}\)

结论:\(f_x = f_y + n - 2 \times siz_x\),其中 \(x\)\(y\) 的儿子。

为什么呢?当以 \(y\) 为根的变为 \(x\) 为根,那么 \(x\) 所有的儿子的深度就会减 \(1\),也就是整体减去 \(siz_x\),那么其他不在 \(x\) 儿子的节点的深度就会加 \(1\),也就是整体加 \((n - siz_x)\),那么 \(f_x = f_y - siz_x + (n - siz_x) = f_x = f_y + n - 2 \times siz_x\)。而 \(siz_x\) 我们可以一次 dfs 得到,时间复杂度 \(O(n)\)

点击查看代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int MAXN = 7 * 1e6 + 100;

vector<int> G[MAXN];

long long dis[MAXN], sum, a[MAXN], id = 1, size[MAXN], n;

void dfs(int u) {
  size[u] = 1;
  for (auto i : G[u]) {
    if (dis[i]) {
      continue;
    } 
    dis[i] = dis[u] + 1;
    dfs(i);
    size[u] += size[i];
  }
}

void dfs1(int u, int fa) {
  a[u] = a[fa] + n - 2 * size[u];
  for (auto i : G[u]) {
    if (i == fa) {
      continue;
    }
    dfs1(i, u);
  }
}

signed main() {
  cin >> n;
  for (int i = 1; i < n; i++) {
  	int x, y;
  	cin >> x >> y;
  	G[x].push_back(y);
  	G[y].push_back(x);
  }
  dis[1] = 1;
  dfs(1);
  for (int i = 1; i <= n; i++) {
  	sum += dis[i];
  } 
  sum -= n;
  a[1] = sum;
  for (int i = 0; i < G[1].size(); i++) {
    int v = G[1][i];
	  dfs1(v, 1);
  }
  for (int i = 1; i <= n; i++) {
    if (a[i] > sum) {
      sum = a[i];
      id = i;
	  }
  }
  cout << id << endl;
  return 0;
}

数位 DP

形如问 \([L,R]\) 中有多少个满足条件的数,考虑前缀和思想:\(f(R)-f(L-1)\)\(f(x)\) 表示 \([1,x]\) 满足条件的数。

然后我们采用记忆化来写数位 DP。

  • \(x\) 表示当前 dfs 到第 \(x\) 位,注意从高位枚举到低位。

  • \(sum\) 表示第 \(x\) 位的答案。

  • \(vis\) 表示上一位是否是满足条件的最大值,具体的,如果范围是 \(672281\),第 \(1\) 位如果是 \(6\),那么第 \(2\) 位就只能取 \(0\sim 7\) 了,否则第二位就可以取 \(0\sim 9\) 了。

eg:Luogu P4999 烦人的数学作业

点击查看代码
#include <bits/stdc++.h>
#define int unsigned long long

using namespace std;

const int N = 1e5 + 100;
const int mod = 1e9 + 7;

int L, R, a[N], f[200][200], t, cnt;

int dfs(int x, int sum, bool vis) {
//	cout << x << endl;
	if (!x) { // 第 0 位
		return sum % mod;
	}
	if (!vis && f[x][sum]) { // 有答案了
		return f[x][sum] % mod;
	}
	int r = vis ? a[x] : 9, ans = 0; // 具体见 vis 的定义
	for (int i = 0; i <= r; i++) {
		ans = (ans + dfs(x - 1, sum + i, vis && i == r ? 1 : 0) % mod) % mod; // 从高位开始,数码和 + i,vis 的大小
	}
	if (!vis) { // 记忆化
		f[x][sum] = ans % mod;
	}
	return ans % mod;
}

int solve(int n) { // 分解
	cnt = 0;
	memset(a, 0, sizeof(a));
	while (n) {
		a[++cnt] = n % 10;
		n /= 10;
	}
	return dfs(cnt, 0, 1) % mod;
}

signed main() {
	cin >> t;
	while (t--) {
		cin >> L >> R;
		cout << (solve(R) - solve(L - 1) + mod) % mod << endl; 
	}
	return 0;
}

好题:

luogu P2224 [HNOI2001] 产品加工

\(dp_{i,j}\) 为前 \(i\) 个物品 \(A\) 机器做了 \(j\) 分钟时,\(B\) 机器的最少时间。

然后我们分三种情况讨论:

当前物品为 \(k\)

  • \(A\) 做,\(B\) 的时间不变,为 \(dp_{i-1,j-t1_i}\)

  • \(B\) 做,那么 \(A\) 的时间不变,\(dp_{i,j}=dp_{i-1,j}+t2_i\)

  • \(A,B\) 同时做,那么时间都要变,\(dp_{i,j}=dp_{i-1,j-t3_i}+t3_i\)

初始化:\(dp\) 数组全部赋成极大值,显然 \(dp_{0,0}=0\)

答案:\(\min\limits_{i=1}^n\{\max\{i,dp_{n,i}\}\}\)

但是题目的空间限制为 \(125 \texttt{MB}\),肯定会 \(\texttt{MLE}\),那么怎么优化呢?

观察到 \(dp_{i,j}\) 都是从 \(dp_{i-1,x}\) 推来的,所以我们类似 01 背包的想法,把第一维 \(i\) 滚掉,把 \(j\) 的循环从大到小就可以了。

点击查看代码
#include <bits/stdc++.h>

using namespace std;

const int MAXN = 2e5 + 100;

int n, f[MAXN], t1, t2, t3, ans = 2e9;

// f[i][j] 表示前 i 个物品,A 机器做了 j 分钟,B 机器做的最小值
// f[i][j] = max({f[i - 1][j] + t2[i], f[i - 1][j - t3[i]] + t3[i], f[i - 1][j - t1[i]});

// 空间 125 MB 会 MLE,优化掉一维,j 循坏改从大到小

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for (int k = 1; k <= n; ++k) {
    cin >> t1 >> t2 >> t3;
    for (int j = 20000; j >= 0; --j) {
      if (t2) {
		f[j] += t2;
	  } else {
		f[j] = 2e9;
	  }
	  if (t1 && j >= t1) {
        f[j] = min(f[j], f[j - t1]);
	  }
	  if (t3 && j >= t3) {
		f[j] = min(f[j], f[j - t3] + t3);
	  }
    }
  }
  for (int i = 0; i <= 20000; i++) {
    ans = min(ans, max(i, f[i]));
  }
  cout << ans;
  return 0;
}

luogu P7914 [CSP-S 2021] 括号序列

\(dp_{i,j,0}\) 表示 \(i \sim j\) 中形如 \(\texttt{**...***}\) (均为 \(\texttt{*}\))的答案。

\(dp_{i,j,1}\) 表示 \(i \sim j\) 中形如 \(\texttt{(...)}\) (括号匹配) 的答案。

\(dp_{i,j,2}\) 表示 \(i \sim j\) 中形如 \(\texttt{(...)***(...)***}\) (左起括号,右接 \(\texttt{*}\))的答案。

\(dp_{i,j,3}\) 表示 \(i \sim j\) 中形如 \(\texttt{(...)***(...)***(...)}\) (左起括号,右接括号)的答案。

\(dp_{i,j,4}\) 表示 \(i \sim j\) 中形如 \(\texttt{***(...)***(...)***}\) (左起 \(\texttt{*}\),右接 \(\texttt{*}\))的答案。

考虑怎么转移:

  • \(s_j\)\(\texttt{*}\)\(\texttt{?}\) 时, \(dp_{i,j,0} = dp_{i,j-1,0}\)

  • \(dp_{i,j,1}\) 可以包括其他合法的序列,但是要满足括号要匹配,\(dp_{i,j,1} = dp_{i-1,j+1,0} + dp_{i-1,j+1,2} + dp_{i-1,j+1,3} + dp_{i-1,j+1,4}\)

  • 根据 \(dp_{i,j,2}\) 的定义,可以分成 \(\texttt{(...)***(...)***(...)}\) 加上一个 \(\texttt{(...)}\),根据乘法原理,也就是 \(dp_{i,j,2} = \sum\limits_{t=i}^{j-1}dp_{i,j,3} \times dp_{i,j,0}\)

  • 左起括号,右边随便,只有 \(2,3\) 符合(\(3\) 包含 \(1\)),所以状态转移方程为 \(dp_{i,j,3}=\sum\limits_{t=l}^{r-1}((dp_{i,t,2} + dp_{i,t,3} \times dp_{t+1,j,1})) + dp_{i,j,1}\),乘上 \(dp_{i,t,3}\) 是因为 \(dp_{i,j,3}\) 的定义是括号结尾,所以要加上一个括号。\(dp_{i,j,1}\) 的话就是加上 \(\texttt{(...)}\) 的情况。

  • 发现 \(dp_{i,j,4}\) 的定义其实是 \(dp_{i,j,2}\) 的定义的逆序列,所以方程式的位置调换一下即可,也就是 \(dp_{i,j,4} = \sum\limits_{t=l}^{r-1}dp_{i,t,0} \times dp_{t+1,j,3}\)

答案:\(dp_{1,n,3}\)

初始化:\(dp_{i,i-1,0}=1\)

注:代码中 \(dp\) 数组的下标比上面的思路增加 \(1\)

点击查看代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int MAXN = 5e2 + 100;
const int mod = 1e9 + 7;

int n, k, dp[MAXN][MAXN][6];
string s;

// dp[i][j][1] i ~ j **..**
// dp[i][j][2] i ~ j (...)
// dp[i][j][3] i ~ j (...)***(...)***
// dp[i][j][4] i ~ j (...)***(...)***(...)
// dp[i][j][5] i ~ j ***(...)***(...)

bool check(int x, int y) {
  return (s[x] == '?' || s[x] == '(') && (s[y] == '?' || s[y] == ')');
}

signed main() {
  cin >> n >> k >> s;
  s = ' ' + s;
  for (int i = 1; i <= n; i++) {
    dp[i][i - 1][1] = 1;
  }
  for (int l = 2; l <= n; l++) {
	for (int i = 1; i + l - 1 <= n; i++) {
	  int j = i + l - 1;
	  if (l <= k && (s[j] == '*' || s[j] == '?')) {
		dp[i][j][1] = dp[i][j - 1][1];
	  } 
	  if (l < 2) {
	    continue;
	  }
	  dp[i][j][2] = check(i, j) * (dp[i + 1][j - 1][1] + dp[i + 1][j - 1][3] + dp[i + 1][j - 1][4] + dp[i + 1][j - 1][5]) % mod;
	  for (int t = i; t < j; t++) {
	    dp[i][j][3] = (dp[i][j][3] + (dp[i][t][4] * dp[t + 1][j][1]) % mod) % mod;
		dp[i][j][4] = (dp[i][j][4] + (dp[i][t][3] + dp[i][t][4]) * dp[t + 1][j][2]) % mod;
		dp[i][j][5] = (dp[i][j][5] + (dp[i][t][1] * dp[t + 1][j][4] % mod) % mod);
	  }
	  dp[i][j][4] = (dp[i][j][4] + dp[i][j][2]) % mod;
    }
  }
//	for (int i = 1; i <= n; i++) {
//	  for (int j = 1; j <= n; j++) {
//	    for (int k = 1; k <= 4; k++) {
//	      cout << "k=" << k << " i=" << i << " j=" << j << " =" << dp[i][j][k] << endl;
//	    }
//	  }
//	}
  cout << dp[1][n][4] % mod << endl;
  return 0;
}

luogu P6280 [USACO20OPEN] Exercise G

明显 \(k = \text{lcm}\{a_i\}\),其中 \(a_i\) 为环的数量。

我们设 \(dp_{i,j}\) 为前 \(i\) 个质数和为 \(j\)\(k\) 的方案数,显然我们可以枚举每一个质数 \(p\) 的指数幂 \(p_{k}^i\),显然 \(dp_{i,j}=\sum dp_{i-1,j-p_{k}^i} \times p_{k}^i\)。然后按照套路发现 \(dp_{i,x}\)\(dp_{i-1,y}\) 转移而来,所以我们可以滚动数组优化掉一维,\(j\) 改为从大到小。

点击查看代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int MAXN = 1e3 + 1;

int n, m, f[MAXN], b[MAXN], cnt;
bool vis[MAXN];

void init() {
  for (int i = 2; i <= MAXN; i++) {
    if (!vis[i]) {
      b[++cnt] = i;
    }
    for (int j = 1; j <= cnt && b[j] * i <= MAXN; j++) {
      vis[i * b[j]] = 1;
      if (i % b[j] == 0){
        break;
      }
    }
  } 
}

signed main() {
  cin >> n;
  init();
  f[0] = 1;
  for (int i = 1; i <= cnt; i++) {
    for (int j = n; j >= b[i]; j--) {
      for (int k = b[i]; k <= n; k *= b[i]) {
        if (j >= k) {
          f[j] += f[j - k] * k;
        }
      }
    }
  }
  int sum = 0;
  for (int i = 0; i <= n; i++) {
    sum += f[i];
//    cout << f[i] << ' ';
  }
//  cout << '\n';
//  cout << f[n];
  cout << sum << endl;
  return 0;
}


codeforces 710E

难度:*2000

\(f_i\) 表示长度为 \(i\) 的字符串的最小代价:

如果 \(i\) 是偶数:\(f_i = min(f_{i - 1} + x, f_{\frac{i}{2}} + y)\)

因为你可以从 \(i - 1\) 直接加上一个字符,代价为 \(x\),也可以从 \(\frac{i}{2}\) 直接翻倍,代价为 \(y\)

如果 \(i\) 是奇数:\(f_i = min(f_{i - 1} + x, f_{\frac{i}{2} + 1} + y + x)\)

\(i - 1\) 部分和上面一样,然而你也可以从 \(\frac{i}{2} + 1\) 处直接翻倍,但是这样会多出一个字符,必须花费 \(x\) 才能消除。

点击查看代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 1e7 + 100;

int n, x, y, dp[N];
// dp[i] 表示长度为 i 的字符串的最小代价:
// i 是偶数:min(dp[i - 1] + x, dp[i / 2] + y)
// i 是奇数:min(dp[i - 1] + x, dp[i / 2 + 1] + y + x)

signed main() {
	cin >> n >> x >> y;
	memset(dp, 127, sizeof(dp));
	dp[0] = 0;
	for (int i = 1; i <= n; i++) {
		if (i & 1) {
			dp[i] = min(dp[i - 1] + x, dp[i / 2 + 1] + y + x);
		} else {
			dp[i] = min(dp[i - 1] + x, dp[i / 2] + y);
			// dp[1] = 62
			// dp[2] = 
		}
	}
	cout << dp[n];
	return 0;
}

参考:oi-wiki dp 专题题解 P1433 【吃奶酪】题解 P7914 【[CSP-S 2021] 括号序列】

posted @ 2023-08-19 21:17  ydq1101  阅读(217)  评论(0)    收藏  举报