2023.3.12
训练
本周进行了一场ABC以及组队赛训练,同时学习了数位DP,巩固了线性DP,并学习了部分有关树的知识内容
学习内容
线性DP
数位DP
线性DP
关键在于使用了所有的牌就一定到达终点, 定义dp[a][b][c][d]表示每种牌用了几张, 而且也可以推出此时到了哪一个位置, 但是从哪一个状态转移过来的, 需要进行讨论, 由于从底层递推, 所以能保证前一个状态已经是最佳状态, 直接进行递推即可, 此题若使用dfs会TLE, 因为dfs思路是遍历每一种走的情况, 但明显走的情况有非常多种, 逐一遍历的话效率太低, 所以用状态转移实际上利用记录状态来实现记忆化, 提高效率的目的
#include <bits/stdc++.h>
const int maxn = 100010;
const int inf = 0x3f3f3f3f;
using namespace std;
int num[400];
int cnt[5];
int dp[45][45][45][45];
int main()
{
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> num[i];
for (int i = 1; i <= m; i++) {int x; cin >> x; cnt[x]++;}
dp[0][0][0][0] = num[1];
for (int a = 0; a <= cnt[1]; a++)
for (int b = 0; b <= cnt[2]; b++)
for (int c = 0; c <= cnt[3]; c++)
for (int d = 0; d <= cnt[4]; d++)
{
int r = 1 + a + 2 * b + 3 * c + 4 * d;
if (a) dp[a][b][c][d] = max(dp[a][b][c][d], dp[a - 1][b][c][d] + num[r]);
if (b) dp[a][b][c][d] = max(dp[a][b][c][d], dp[a][b - 1][c][d] + num[r]);
if (c) dp[a][b][c][d] = max(dp[a][b][c][d], dp[a][b][c - 1][d] + num[r]);
if (d) dp[a][b][c][d] = max(dp[a][b][c][d], dp[a][b][c][d - 1] + num[r]);
}
cout << dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]] << endl;
return 0;
}
出现两个字符串, 需要进行dp操作时, 往往可以先考虑将遍历到第几个字符设置进dp数组, 但是此题还包括k个子串, 所以还需要把k设置进dp数组, 然后开始考虑状态转移, 初始想法是dp[i][j][k] = dp[i - 1][j - 1][k] - 1, dp[i - 1][j - 1][k], 表示A串前i个字符使用了k个子串, 匹配了B串前j个字符的方案数, 等价于单独使用第i个作为一个字串的方案数以及不单独使用的方案数, 但是不单独使用包括不使用以及使用了只是和前面连在了一起不算新的子串, 所以单独这一个dp状态转移方程不足以判断所有情况, 还需要添加一维, 表示有无使用第i个字符, 从而构造dp状态转移方程为dp[i][j][k][0] = dp[i - 1][j][k][0] + dp[i - 1][j][k][1] 以及 if (a[i] == b[j]) dp[i][j][k][1] = dp[i - 1][j - 1][k - 1][0] + dp[i - 1][j - 1][k][1] + dp[i - 1][j - 1][k - 1][1], 然后状态转移方程列出后, 还要考虑边界问题, 显然dp[i][1][1][1] = 1, dp[i][1][1][0] = sum(a[1 ~ i - 1] == b[1]), 最后考虑到每次转移时只与i - 1的状态有关, 所以可以进行滚动, 优化空间复杂度; 同时题目还有取模操作, 要防止数据溢出和注意随时取模; 方案数转移时是直接相加
#include <bits/stdc++.h>
const int mod = 1000000007;
using namespace std;
char a[1010], b[210];
int n, m, t, dp[2][210][210][2], s, now, old = 1;
int main()
{
scanf("%d%d%d", &n, &m, &t);
scanf("%s%s", a + 1, b + 1);
for (int i = 1; i <= n; i++)
{
swap(now, old);
dp[now][1][1][0] = s;
if (a[i] == b[1]) s++, dp[now][1][1][1] = 1;
for (int j = 2; j <= m; j++)
{
for (int k = 1; k <= t; k++)
{
if (a[i] == b[j])
dp[now][j][k][1] = ((dp[old][j - 1][k][1] + dp[old][j - 1][k - 1][1]) % mod + dp[old][j - 1][k - 1][0]) % mod;
dp[now][j][k][0] = (dp[old][j][k][1] + dp[old][j][k][0]) % mod;
}
}
for (int j = 1; j <= m; j++)
for (int k = 1; k <= t; k++)
dp[old][j][k][1] = dp[old][j][k][0] = 0;
}
printf("%d", (dp[now][m][t][1] + dp[now][m][t][0]) % mod);
return 0;
}
Bridging signals - POJ 1631 - Virtual Judge (csgrandeur.cn)
求最长上升子序列, 若使用dp思路, 在$O^2$级别的复杂度下会导致TLE, 所以使用单调栈优化, 此算法似乎已经不算dp了, 更像是贪心, 复杂度为$nlogn$, 以求最长上升子序列为例, 则维护的是单调递增栈, 每次取栈顶元素top和读到的元素temp比较, 如果temp > top, 则入栈, 否则使用二分查找到第一个第一个比temp大的数, 然后替换即可, 可以理解为使得该上升子序列"潜力"增大, 但是有时候会得不到真实最长上升子序列, 例如1, 5, 8, 2, 维护单调递增栈最后会得到1, 2, 8, 实际上并不是真正的最长上升子序列, 但是个数是正确的, 这是很好理解的, 也就是说, 如果仅仅求个数, 则可以使用单调栈, 但是如果需要我们打印最长上升子序列, 则只能用dp
//Accepted
#include <iostream>
#include <cstring>
#include <algorithm>
const int maxn = 1e4 + 10;
const int inf = 0x3f3f3f3f;
using namespace std;
int dp[40010];
int a[40010];
int main()
{
int t; cin >> t;
while (t--)
{
int n; cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++)
*lower_bound(dp, dp + n, a[i]) = a[i];
cout << (lower_bound(dp, dp + n, inf) - dp) << endl;
}
return 0;
}
//TLE
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
const int maxn = 1e4 + 10;
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int t; cin >> t;
while (t--)
{
int n; cin >> n;
vector<pair<int, int>> ve;
int dp[40010], ans = 0;
memset(dp, 0, sizeof dp);
for (int i = 1; i <= n; i++)
{
int x; cin >> x;
ve.push_back(pair<int, int>(i, x));
dp[i] = 1;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
{
if (ve[i].first > ve[j].first && ve[i].second > ve[j].second) dp[i] = max(dp[j] + 1, dp[i]);
ans = max(ans, dp[i]);
}
cout << ans << endl;
}
return 0;
}
此题跟前一题思路一致, 但是有些细节需要注意, 在求最长不降子序列的时候, 不能使用lower_bound(), 要使用upper_bound(), 只有求最长上升子序列才是使用lower_bound
#include <bits/stdc++.h>
const int maxn = 100010;
const int inf = 0x3f3f3f3f;
using namespace std;
int x, num[maxn];
int dp[maxn];
int main()
{
int cnt = 0;
while (cin >> x) num[++cnt] = x;
memset(dp, 0x3f, sizeof dp);
for (int i = cnt; i >= 1; i--)
*upper_bound(dp, dp + cnt, num[i]) = num[i];
cout << (lower_bound(dp, dp + cnt, inf) - dp) << endl;
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= cnt; i++)
*lower_bound(dp, dp + cnt, num[i]) = num[i];
cout << (lower_bound(dp, dp + cnt, inf) - dp) << endl;
return 0;
}
Space Elevator - POJ 2392 - Virtual Judge (csgrandeur.cn)
多重背包, 但是有高度限制, 注意状态转移的时候, 对类型进行遍历时, 要对限制高度较低的先处理, 才能保证dp转移顺利进行
#include <iostream>
#include <algorithm>
using namespace std;
int dp[40010];
struct str
{
int w, c, lm;
bool operator < (const str &a) const
{return lm < a.lm;}
}bl[410];
int main()
{
int n; cin >> n;
for (int i = 1; i <= n; i++)
cin >> bl[i].w >> bl[i].lm >> bl[i].c;
sort(bl + 1, bl + 1 + n);
dp[0] = 1;
int ans = 0;
for (int i = 1; i <= n; i++)
for (int k = 1; k <= bl[i].c; k++)
for (int j = bl[i].lm; j >= bl[i].w; j--)
{
dp[j] |= dp[j - bl[i].w];
if (dp[j]) ans = max(ans ,j);
}
cout << ans << endl;
return 0;
}
Cow Exhibition - POJ 2184 - Virtual Judge (csgrandeur.cn)
看作01背包问题, 本题有两个信息, ts[i], tf[i], 而且都有可能是负数, 所以要进行位移处理, 将dp[100000]作为基准, 左端为负, 右端为正, 定义dp时, 视作花费ts[i]元获得tf[i]的价值, 维护价值最大, 最后再对大于0的进行遍历即可. 本题最巧妙的就是把两个值转换为花费和价值, 使之能够进行dp操作
#include <iostream>
#include <algorithm>
#include <cstring>
const int maxn = 1e4 + 10;
const int inf = 0x3f3f3f3f;
using namespace std;
int dp[200001], ts[200001],tf[200001];
int main()
{
int n, ans = 0;
memset(dp, -0x3f, sizeof dp);
cin >> n;
dp[100000] = 0;
for (int i = 1; i <= n; i++) cin >> ts[i] >> tf[i];
for (int i = 1; i <= n; i++)
{
if (ts[i] >= 0)
{
for (int j = 200000; j >= ts[i]; j--)
dp[j] = max(dp[j], dp[j - ts[i]] + tf[i]);
}
else
{
for (int j = 0; j - ts[i] <= 200000; j++)
dp[j] = max(dp[j], dp[j - ts[i]] + tf[i]);
}
}
for (int i = 100000; i <= 200000; i++)
if (dp[i] >= 0) ans = max(ans, i - 100000 + dp[i]);
cout << ans << endl;
return 0;
}
数位统计DP
用于数字的数位统计. 一个数字的数位有个位、十位、百位等等,如果题目和数位统计有关,那么可以用DP思想,把低位的统计结果记录下来,在高位计算时直接使用低位的结果,从而提高效率
数位统计有关的题目,基本内容是处理“前导0”和“数位限制”
递推实现: 定义dp[i]为i位数的每种数字有多少个
记忆化搜索实现: 定义dp[pos][sum] 分别表示最后pos位范围, 前面now的个数, 为了简便也可以定义为dp[pos][sum][limit][lead], 后面表示有无数位限制和前导0, 下面是dfs思路
//递推实现
#include <bits/stdc++.h>
const int maxn = 20;
typedef long long LL;
using namespace std;
LL ten[maxn], dp[maxn];
LL cnta[maxn], cntb[maxn]; //cnt[i]统计数字i出现了几次
int num[maxn];
void init() //预计算dp[]
{
ten[0] = 1; //ten[i] 10的i次方
for (int i = 1; i <= 15; i++)
{
dp[i] = i * ten[i - 1];
ten[i] = 10 * ten[i - 1];
}
}
void solve(LL x, LL *cnt)
{
int len = 0;
while (x) //数字x有多少位
{
num[++len] = x % 10; //分解x, num[i]为x的第i位数字
x /= 10;
}
for (int i = len; i >= 1; i--) //从高到低处理x的每位
{
for (int j = 0; j <= 9; j++) cnt[j] += dp[i - 1] * num[i];
for (int j = 0; j < num[i]; j++) cnt[j] += ten[i - 1]; //特判最高位比num[i]小的数字
LL num2 = 0;
for (int j = i - 1; j >= 1; j--) num2 = num2 * 10 + num[j];
cnt[num[i]] += num2 + 1; //特判最高位的数字num[i]
cnt[0] -= ten[i - 1]; //特判前导0
}
}
int main()
{
init();
LL a, b; cin >> a >> b;
solve(a - 1, cnta), solve(b, cntb);
for (int i = 0; i <= 9; i++) cout << cntb[i] - cnta[i] << " ";
return 0;
}
//记忆化搜索实现
#include <bits/stdc++.h>
const int N = 20;
typedef long long LL;
using namespace std;
LL dp[N][N];
int num[N], now;
LL dfs(int pos, int sum, bool lead, bool limit)
{
LL ans = 0;
if (pos == 0) return sum;
if (!lead && !limit && dp[pos][sum] != -1) return dp[pos][sum];
int up = (limit ? num[pos] : 9);
for (int i = 0; i <= up; i++)
{
if (i == 0 && lead) ans += dfs(pos - 1, sum, true, limit && i == up);
else if (i == now) ans += dfs(pos - 1, sum + 1, false, limit && i == up);
else if (i != now) ans += dfs(pos - 1, sum, false, limit && i == up);
}
if (!lead && !limit) dp[pos][sum] = ans;
return ans;
}
LL solve(LL x)
{
int len = 0;
while (x)
{
num[++len] = x % 10;
x /= 10;
}
memset(dp, -1, sizeof dp);
return dfs(len, 0, true, true);
}
int main()
{
LL a, b; cin >> a >> b;
for (int i = 0; i < 10; i++) now = i, cout << solve(b) - solve(a - 1) << " ";
return 0;
}
//记忆化搜素优化写法, 把limit和lead也放入dp记录下来
LL dp[N][N][2][2];
LL dfs(int pos, int sum, bool lead, bool limit)
{
LL ans = 0;
if (pos == 0) return sum;
if (dp[pos][sum][limit][lead] != -1) return dp[pos][sum][limit][lead];
int up = (limit ? num[pos] : 9);
for (int i = 0; i <= up; i++)
{
if (i == 0 && lead) ans += dfs(pos - 1, sum, true, limit && i == up);
else if (i == now) ans += dfs(pos - 1, sum + 1, false, limit && i == up);
else if (i != now) ans += dfs(pos - 1, sum, false, limit && i == up);
}
dp[pos][sum][limit][lead] = ans;
return ans;
}
定义dp[pos][last], 代表数字长度为pos位, 前一位是last的无数位限制的Windy数总数
#include <bits/stdc++.h>
const int N = 20;
typedef long long LL;
using namespace std;
int dp[N][N][2];
int num[N];
int dfs(int pos, int last, bool lead, bool limit)
{
int ans = 0;
if (pos == 0) return 1;
if (dp[pos][last][limit] != -1) return dp[pos][last][limit];
int up = (limit ? num[pos] : 9);
for (int i = 0; i <= up; i++)
{
if (abs(i - last) < 2) continue;
if (i == 0 && lead) ans += dfs(pos - 1, -2, true, limit && i == up);
else ans += dfs(pos - 1, i, false, limit && i == up);
}
dp[pos][last][limit] = ans;
return ans;
}
int solve(int x)
{
int len = 0;
while (x)
{
num[++len] = x % 10;
x /= 10;
}
memset(dp, -1, sizeof dp);
return dfs(len, -2, true, true);
}
int main()
{
int a, b; cin >> a >> b;
cout << solve(b) - solve(a - 1) << endl;
return 0;
}
注意定义dp状态时不要遗漏每个信息
#include <bits/stdc++.h>
const int N = 20;
typedef long long LL;
using namespace std;
int num[N];
int dp[15][11][11][2][2][2];
LL dfs(int pos, int u, int v, bool state, bool n8, bool n4, bool limit)
{
LL ans = 0;
if (n8 && n4) return 0;
if (!pos) return state;
if (!limit && dp[pos][u][v][state][n8][n4] != -1) return dp[pos][u][v][state][n8][n4];
int up = (limit ? num[pos] : 9);
for (int i = 0; i <= up; i++)
ans += dfs(pos - 1, i, u, state || (i == u && u == v), n8 || (i == 8), n4 || (i == 4), limit && i == up);
if (!limit) dp[pos][u][v][state][n8][n4] = ans;
return ans;
}
LL solve(LL x)
{
int len = 0;
while (x)
{
num[++len] = x % 10;
x /= 10;
}
if (len != 11) return 0;
memset(dp, -1, sizeof dp);
LL ans = 0;
for (int i = 1; i <= num[len]; i++)
ans += dfs(len - 1, i, 0, 0, i == 8, i == 4, i == num[len]);
return ans;
}
int main()
{
LL a, b; cin >> a >> b;
cout << solve(b) - solve(a - 1) << endl;
return 0;
}
本题思路十分巧妙, 值得反复学习. 符合要求的数就是可以被它的每一位的数字整除的数, 给定一个区间, 求符合要求的数的个数; 分析: 一个数能被它的所有非零数位整除, 则能被它们的最小公倍数lcm整除, 而1到9的最小公倍数是2520, 数位dp时我们需要保存前面那些位的最小公倍数然后进行状态转移, 到边界时就把所有位的lcm求出了, 为了判断这个数能否被它的所有数位整除, 还需要存这个数的值, 但是直接存是不可能的, 因为数字太大了, 其实只需要记录它对2520的模即可, 这样就可以设计出数位dp: dfs(pos, preSum, preLcm, limit), pos为当前数位, preSum为前面那些位数对2520的模, preLcm为前面那些数位的最小公倍数, limit为高位限制, 但是这样的话dp数组要开到[19][2520][2520], 会超内存, 考虑到最小公倍数是离散的, $1-2520$中可能的最小公倍数其实只有48个, 离散化处理之后, dp数组的最后一维可以降到48, 实现最大优化
注意本题处理时还有一些数论知识, 涉及求最大公倍数和最小公倍数, 具体参见代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 25;
const int mod = 2520;
LL dp[maxn][mod][48];
int index[mod + 10];
int num[maxn];
void init()
{
memset(dp, -1, sizeof dp);
int num = 0;
for (int i = 1; i <= mod; i++) //离散化处理
if (mod % i == 0) index[i] = num++;
}
int gcd(int a, int b) //求最大公约数
{
if (b == 0) return a;
else return gcd(b, a % b);
}
int lcm(int a, int b) //求最小公倍数
{
return a / gcd(a, b) * b;
}
LL dfs(int pos, int preSum, int preLcm, bool limit)
{
if (pos == 0) return preSum % preLcm == 0;
if (!limit && dp[pos][preSum][index[preLcm]] != -1)
return dp[pos][preSum][index[preLcm]];
LL ans = 0;
int up = limit ? num[pos] : 9;
for (int i = 0; i <= up; i++)
{
int nowSum = (preSum * 10 + i) % mod;
int nowLcm = preLcm;
if (i) nowLcm = lcm(nowLcm, i);
ans += dfs(pos - 1, nowSum, nowLcm, limit && i == up);
}
if (!limit) dp[pos][preSum][index[preLcm]] = ans;
return ans;
}
LL solve(LL x)
{
int len = 0;
while (x)
{
num[++len] = x % 10;
x /= 10;
}
return dfs(len, 0, 1, 1);
}
int main()
{
int t; cin >> t;
init();
LL l, r;
while (t--)
{
cin >> l >> r;
cout << solve(r) - solve(l - 1) << endl;
}
return 0;
}
3252 -- Round Numbers (poj.org)
#include <iostream>
#include <cstring>
using namespace std;
int num[30];
int dp[70][35];
int dfs(int pos, int pre, bool lead, bool limit)
{
if (pos == 0) return pre <= 32;
if (dp[pre][pos] != -1 && !lead && !limit) return dp[pre][pos];
int ans = 0;
int up = limit ? num[pos] : 1;
for (int i = 0; i <= up; i++)
{
if (i == 0 && lead) ans += dfs(pos - 1, pre, 1, limit && i == up);
else ans += dfs(pos - 1, pre + (i == 0 ? -1 : 1), 0, limit && i == up);
}
if (!limit && !lead) dp[pre][pos] = ans;
return ans;
}
int solve(int x)
{
int len = 0;
memset(dp, -1, sizeof dp);
while (x)
{
num[++len] = x % 2;
x /= 2;
}
return dfs(len, 32, 1, 1);
}
int main()
{
int a, b; cin >> a >> b;
cout << solve(b) - solve(a - 1) << endl;
return 0;
}
Dollar Dayz - POJ 3181 - Virtual Judge
完全背包+大数(高精度), 因为数据范围会超long long, 所以开二维dp, dp[i][0]存放大数的前半段, dp[i][1]存放大数的后半段, 处理手法值得借鉴
#include <iostream>
#define LIMIT_ULL 100000000000000000
const int maxn = 1e4 + 10;
using namespace std;
long long dp[1010][2];
int main()
{
int n, k; cin >> n >> k;
dp[0][1] = 1;
for (int i = 1; i <= k; i++)
for (int j = i; j <= n; j++)
{
if (dp[j - i][1])
{
dp[j][0] += dp[j - i][0];
dp[j][1] += dp[j - i][1];
dp[j][0] += dp[j][1] / LIMIT_ULL;
dp[j][1] = dp[j][1] % LIMIT_ULL;
}
}
if (dp[n][0]) cout << dp[n][0];
cout << dp[n][1] << endl;
return 0;
}