基础 DP 做题记录
Luogu P1192 台阶问题 Link
简要题意:
给定台阶数 \(n\le10^5\) 和一步至多跨越台阶数 \(k\le10^2\) ,初始在 \(0\) 级,求方案数 \(\pmod {10^5+3}\)。
思路:
设 \(f_i\) 表示走到第 \(i\) 级台阶的方案数,题意直接说明了可以从前 \(k\) 级台阶转移过来,考虑每次在以经处理好的台阶前新加一级产生的影响就是对于之后的 \(k\) 级的每一种方案都新产生了一种方案。所以有转移方程:
\]
最后答案就是 \(f_n\)。
暴力转移即可通过,复杂度 \(O(kn)\)。
也可以对 \(f\) 前缀和优化成 \(O(n)\) 。
$O(n)$ 前缀和优化代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10, mo = 1e5 + 3;
int f[maxn], s[maxn], n, k;
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> k;
f[1] = 1, s[1] = 1;
for(int i = 2; i <= n + 1; i++) {
f[i] = (s[i - 1] - s[max(0, i - k - 1)] + mo) % mo;
s[i] = (s[i - 1] + f[i]) % mo;
}
cout << f[n + 1];
}
Luogu P1091 合唱队形 Link
简要题意:
给出 \(n\le10^2\) 和 \(n\) 个有序身高 \(t_i\),求出最小的 \(k\) 使得除去 \(k\) 个人剩下 \(n-k\) 个身高形成先严格单增再严格单减的序列。
思路:
无论是从起始点还是终止点开始考虑都很困难,再加上枚举中间点朴素转移可以达到 \(O(n^5)\) 的时间复杂度。这是不能接受的。考虑若分别求出从一点结尾的最长上升子序列和从一点开始的最长下降子序列,把两段拼起来 \(-1\) 就能得到所有中间点构成的符合题意的序列长度。其中的最大值即 \(n-k\) 的最大值,这样就求出了最小的 \(k\) 。
求最长上升子序列和下降子序列可以做到 \(O(n\log n)\),当然朴素的 \(O(n^2)\) 也能通过。
$O(n^2)$ 朴素dp代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e2 + 5;
int n, h[maxn];
int f1[maxn], f2[maxn];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> h[i];
for(int i = 1; i <= n; i++){
f1[i] = 1;
for(int j = 1; j < i; j++) {
if(h[j] < h[i]) f1[i] = max(f1[i], f1[j] + 1);
}
}
for(int i = n; i >= 1; i--){
f2[i] = 1;
for(int j = i + 1; j <= n; j++) {
if(h[j] < h[i]) f2[i] = max(f2[i], f2[j] + 1);
}
}
int res = 0;
for(int i = 1; i <= n; i++) res = max(res, f1[i] + f2[i] - 1);
cout << n - res;
}
Luogu P1280 尼克的任务 Link
简要题意:
有 \(k\le10^4\) 个任务,分布在时间 \(n\le10^4\) 中,给出每个任务的起始时间 \(l\) 和持续时长 \(t\) (左开右闭),要求从 \(t=1\) 开始有任务起始时必须选择一个任务做,求最长空闲时间。
思路:
考虑把最长休息时间作为状态,如果正序枚举,发现选择做任务会对之后未赋值的状态产生影响,有后效性。所以倒序转移。设 \(f_i\) 表示时间 \(i\) 开始做任务的最长休息时间。如果这个时间 \(i\) 没有起始的任务,即这个时间是空闲的,那么直接由时间 \(i+1\) 转移过来;如果有起始的任务,那就在这些任务完成时间中休息时间最长的作为转移。所以有转移方程:
\max \limits_{l_j=i}f_{i+t_j} & \exists l_j=i\\
f_{i+1}+1 & Otherwise.
\end{cases}
\]
最后答案是 \(f_1\) 。
倒序枚举状态,枚举所有任务找起始时间 \(l_j=i\) 的 \(j\) 转移,复杂度 \(O(kn)\)。
实际上可以用桶排序优化找的那一步,复杂度优化到 \(O(n+k)\)。
$O(kn)$ dp代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 10;
int n, k;
int l[maxn], t[maxn];
int f[maxn];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> k;
for(int i = 1; i <= k; i++) cin >> l[i] >> t[i];
for(int i = n; i >= 1; i--) {
bool flag = false;
for(int j = 1; j <= k; j++) {
if(i == l[j]) {
flag = true;
f[i] = max(f[i], f[i + t[j]]);
}
}
if(!flag) f[i] = f[i + 1] + 1;
}
cout << f[1];
}
这个题也可以转换成图论最短(长?)路,这里不多提。
Luogu P1108 低价购买 Link
简要题意:
给定 \(n\le5\times 10^3\) 和 \(n\) 个整数 \(a_1\sim a_n\),求最长下降子序列长度和这个长度下不重复的下降子序列方案数。
思路:
\(n^2\) 最长下降子序列是容易的,由于方案时刻在变化,考虑动态维护,即在找最长下降子序列时维护方案数。设 \(f_i\) 表示以 \(i\) 结尾最长下降子序列长度,\(g_i\) 表示不重复的下降子序列方案数。枚举 \(j<i\),考虑存在相同方案的必要条件是 \(f_i=f_j \wedge a_i=a_j\),我们发现这同样是充分的,如果在每次 \(f_i\) 更新结束之后。此时我们让 \(g_j\) 清空,因为 \(g_i\) 一定包含了 \(g_j\) 的一切情况;对于 \(a_j>a_i\wedge f_i=f_j+1\) 的情况,一定是这次 \(f_i\) 转移过的状态之一,所以 \(g_i\) 要加上 \(g_j\)。整理得到转移方程:
\max \limits_{a_j>a_i}f_{j}+1 & \exists a_j>a_i\\
1 & Otherwise.
\end{cases}
\]
0&f_i=f_j\wedge a_i=a_j \\
\sum \limits_{a_j>a_i\wedge f_i=f_j+1}g_i+g_j & \exists a_j>a_i\wedge f_i=f_j+1\\
1 & Otherwise.
\end{cases}
\]
注意 \(f_i,g_i\) 的转移要在同一个外循环内,即 \(f_i,g_i\) 的 \(i\) 是同步的。
$O(n^2)$ dp代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 5e3 + 10;
int n, a[maxn];
int f[maxn];
ll g[maxn];
int main() {
ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) {
f[i] = 1;
for(int j = 1; j < i; j++) {
if(a[j] > a[i]) f[i] = max(f[i], f[j] + 1);
}
for(int j = 1; j < i; j++) {
if(a[j] == a[i] && f[j] == f[i]) g[j] = 0;
if(a[j] > a[i] && f[j] + 1 == f[i]) g[i] += g[j];
}
if(!g[i]) g[i] = 1;
}
int cnt = 1; ll ans = 0;
for(int i = 1; i <= n; i++) cnt = max(cnt, f[i]);
for(int i = 1; i <= n; i++) if(f[i] == cnt) ans += g[i];
cout << cnt << " " << ans;
return 0;
}
Luogu P2943 Cleaning Up G Link
简要题意:
给定 \(n\le4\times10^4,m\le n\),和 \(n\) 个值域为 \([1,m]\cap N\) 的有序数,现将其划分为连续段,记连续段中出现不同 \(p_i\) 值的个数为 \(k\),每一段的贡献为 \(k^2\)。求划分后的最小贡献。
思路:
由于具体的划分情况不重要,我们直接设 \(f_i\) 表示前 \(i\) 个数划分后的最小贡献,枚举转移过来的状态 \(f_j\),容易得到转移方程:
\]
其中 \(cnt(j,i)\) 表示 \(p_j\) 到 \(p_i\) 不同值的个数,可以开 \(n\) 个桶动态维护,但是这样时间空间复杂度都是 \(O(n^2)\),理论上不能通过极限数据(但是数据过水好像很多人都 \(O(n^2)\) 冲过去了?)。
还要深挖性质。注意到 \(f_i\le i\),又有 \(f_i=f_{j-1}+k^2\),所以 \(k^2<i\)。进一步的,当 \(i=n\) 时 \(k^2<n\)。所以 \(k\) 存在一个上界 \(\sqrt n\) 。我们只用维护 \(\sqrt n\) 个桶就可以找到最小的贡献。时空复杂度 \(O(n\sqrt n)\)。
$O(n\sqrt n)$ dp代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 4e4 + 10, maxsq = 2e2 + 10;
int n, m, a[maxn];
int pos[maxsq], t[maxsq][maxn], cnt[maxsq];
int f[maxn];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) {
for(int j = 1; j * j <= n; j++){
if(!t[j][a[i]]) cnt[j]++; t[j][a[i]]++;
while(cnt[j] > j) {
if(t[j][a[pos[j]]] == 1) cnt[j]--; t[j][a[pos[j]]]--;
pos[j]++;
}
}
f[i] = f[i - 1] + 1;
for(int j = 1; j * j <= n && pos[j]; j++) {
f[i] = min(f[i], f[pos[j] - 1] + j * j);
}
}
cout << f[n];
}
Luogu P5664 Emiya 家今天的饭 Link
简要题意:
给定 \(n\le10^2, m\le2\times10^3\) 和 \(n\times m\) 矩阵,横行纵列分别表示不同的烹饪方法和主要食材,矩阵上每个数表示会做的不同主菜。对于不同的做菜方案有以下限制:1.每个方案至少有一道菜;2.每个烹饪方法互不相同;3.每个菜品数量为 \(k\) 的方案每种主要食材不超过 \(\lfloor\frac k2\rfloor\) 个。求方案数 \(\bmod{998244353}\) 。
思路:
先理解题意,菜品数 \(k\) 应该不超过 \(n\) 且不小于 \(2\) 。对于每个格子的菜,选择了那么同一行的其它菜就不能选了,所以转移时同一行的菜属于同一个过程,根据加法原理可以加起来作为一个整体记为 \(sum_i\) 。发现第三个限制很棘手,菜的数量要分开考虑,每一列的状态也要分开考虑,这样的时间和空间是难以接受的。
考虑容斥,拿所有的方案数减去不合法的方案数。
满足限制一二的所有方案:对于每一行的菜品,包括不取一共有 \(sum_i+1\) 种情况,根据乘法原理有 \(\prod_{i=1}^n(sum_i+1)-1\),其中减去的 \(1\) 的是每一行都不取的情况(第一条限制)。
不合法方案:由于要违反第三限制,我们需要某些列选择的菜品数大于 \(\lfloor\frac k2\rfloor\)。然而这样的列如果存在那么有且仅会只有这一列,因为其它列的菜品数之和小于 \(\lfloor\frac k2\rfloor\)。不妨枚举单独的一列 \(t\) 选择的菜品数为 \(k'\);为了转移的方便,我们考虑前 \(i\) 行其他列选 \(j\) 个。那么我考虑对于 \(f_{i,j,k'}\) 的转移方程:首先,对于在 \(i\) 行中选一种菜,如果选第 \(t\) 列,产生的方案数为 \(f_{i-1,j,k'-1}\times a_{i,t}\);接着,对于不选 \(t\) 列的情况,产生的方案数为 \(f_{i-1,j-1,k'}\times (sum_i-a_{i,t})\);最后,如果不在第 \(i\) 行选菜,继承先前的方案数 \(f_{i-1,j,k'}\)。即有转移方程:
\]
实现时要枚举 \(t,i,j,k'\),最后在总方案数中把 \(k'>j\) 的 \(f_{i,j,k'}\) 减掉就是合法的方案数。
复杂度达到 \(O(mn^3)\)。这对于大部分测试点来说足够,但是无法通过全部数据。
优化:
考虑到 \(j,k'\) 是我们为了找到不合法方案数的指标,实际上只与它们的相对大小有关,而与其具体的值无关。不妨令 \(j'=k'-j\),再将值域整体向右平移 \(n\) 处理掉负指标。一一对应上文的三种情况,产生方案数分别有 \(f_{i-1,j'-1}\times a_{i,t}\)、\(f_{i-1,j'+1}\times (sum_i - a_{i,t})\)、\(f_{i-1,j'}\)。即有新的转移方程:
\]
现在实现时只需要枚举 \(t,i,j'\) 三个指标,总方案数减去 \(1\le j'\le n\) 的 \(f_{i,j'+n}\) 即为答案。
复杂度 \(O(mn^2)\)。可以通过所有数据。
$O(mn^2)$ 优化后代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e2 + 10, maxm = 2e3 + 10, mo = 998244353;
int n, m, a[maxn][maxm];
ll s = 1, sum[maxn], f[maxn][maxn << 1];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
cin >> a[i][j];
(sum[i] += a[i][j]) %= mo;
}
s = 1ll * s * (sum[i] + 1) % mo;
}
(s += mo - 1) %= mo;
for(int t = 1; t <= m; t++) {
f[0][n] = 1;
for(int i = 1; i <= n; i++) {
for(int j = n - i; j <= n + i; j++) {
f[i][j] = f[i - 1][j];
(f[i][j] += 1ll * f[i - 1][j - 1] * a[i][t] % mo) %= mo;
(f[i][j] += 1ll * f[i - 1][j + 1] * (sum[i] - a[i][t]) % mo) %= mo;
}
}
for(int j = 1; j <= n; j++) {
(s += mo - f[n][j + n]) %= mo;
}
memset(f, 0, sizeof f);
}
cout << s;
}
Luogu P2014 选课 Link
简要题意:
有 \(n\) 门课,第 \(i\) 门课有一门先修课 \(k_i\)(\(k_i=0\) 表示没有先修课),选择一门课获得学分 \(s_i\)。现在要选择 \(m\) 门课,每门课必须先选先修课,求获得的学分的最大值。
思路:
发现这些课之间依赖关系构成了森林,但是如果把 \(k_i=0\) 看作是必选的先修课,一共选 \(m+1\) 门课,那依赖关系就形成了一棵树。
考虑如果要选这棵树上的任意一门课,就要把这个节点到根路径上的所有课都选了。如果从一般树形 DP 子树的角度来看这个问题,那么在某一棵子树内选了一门课,那么子树内这个节点到子树的根路径上所有的课都要选。
不妨设 \(f_{u,i}\) 表示在以 \(u\) 为根的子树中选 \(i\) 门课获得的最多学分。这里我们钦定这些课到 \(u\) 以及这棵树的根节点路径上的课都是选好的,只是在 DP 方程里面没有体现。易得 \(f_{u,1}=s_u\) 与 \(f_{u,0} = 0\)。下面我们来考虑根据树的结构来设计转移方程。
在依赖 \(u\) 的几个节点 \(v\) 为根的子树中,我们通过 \(dfs\) 先求出 \(v\) 子树中选不同数量的课得到的最大学分 \(f_{v,j}\)。考虑现在加入了一些子树,再加入子树 \(v\) 时怎么更新答案。类似于 \(0/1\) 背包的,我们枚举背包的容量更新最大的贡献,即:
\]
在 \(dfs\) 时一边更新贡献一边维护子树大小 \(sz\) 即可。注意边界要符合实际意义。
时间复杂度是 \(O(nm)\)。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e2 + 10;
int n, m, s[maxn], f[maxn][maxn];
vector<int> e[maxn];
int dfs(int u) {
int tot = 1;
f[u][1] = s[u];
for(int v : e[u]) {
int sz = dfs(v);
for(int i = min(tot, m + 1); i; i--) {//所有合并过的子树
for(int j = 1; j <= sz && i + j <= m + 1; j++) { //子树v
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
}
}
tot += sz;
}
return tot;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int u; cin >> u >> s[i];
e[u].push_back(i);
} dfs(0);
cout << f[0][m + 1];
return 0;
}
基础 DP 做题记录的更多相关文章
- DP 做题记录 II.
里面会有一些数据结构优化 DP 的题目(如 XI.),以及普通 DP. *I. P3643 [APIO2016]划艇 题意简述:给出序列 \(a_i,b_i\),求出有多少序列 \(c_i\) 满足 ...
- DP刷题记录(持续更新)
DP刷题记录 (本文例题目前大多数都选自算法竞赛进阶指南) TYVJ1071 求两个序列的最长公共上升子序列 设\(f_{i,j}\)表示a中的\(1-i\)与b中色\(1-j\)匹配时所能构成的以\ ...
- Sam做题记录
Sam做题记录 Hihocoder 后缀自动机二·重复旋律5 求一个串中本质不同的子串数 显然,答案是 \(\sum len[i]-len[fa[i]]\) Hihocoder 后缀自动机三·重复旋律 ...
- 退役III次后做题记录(扯淡)
退役III次后做题记录(扯淡) CF607E Cross Sum 计算几何屎题 直接二分一下,算出每条线的位置然后算 注意相对位置这个不能先搞出坐标,直接算角度就行了,不然会卡精度/px flag:计 ...
- BJOI做题记录
BJOI做题记录 终于想起还要做一下历年省选题了2333 然而咕了的还是比做了的多2333 LOJ #2178. 「BJOI2017」机动训练 咕了. LOJ #2179. 「BJOI2017」树的难 ...
- UOJ 做题记录
UOJ 做题记录 其实我这么弱> >根本不会做题呢> > #21. [UR #1]缩进优化 其实想想还是一道非常丝播的题目呢> > 直接对于每个缩进长度统计一遍就好 ...
- project euler做题记录
ProjectEuler_做题记录 简单记录一下. problem 441 The inverse summation of coprime couples 神仙题.考虑答案为: \[\begin{a ...
- 退役IV次后做题记录
退役IV次后做题记录 我啥都不会了.... AGC023 D 如果所有的楼房都在\(S\)同一边可以直接得出答案. 否则考虑最左最右两边的票数,如果左边>=右边,那么最右边会投给左边,因为就算车 ...
- 退役II次后做题记录
退役II次后做题记录 感觉没啥好更的,咕. atcoder1219 历史研究 回滚莫队. [六省联考2017]组合数问题 我是傻逼 按照组合意义等价于\(nk\)个物品,选的物品\(\mod k\) ...
- FJOI2017前做题记录
FJOI2017前做题记录 2017-04-15 [ZJOI2017] 树状数组 问题转化后,变成区间随机将一个数异或一,询问两个位置的值相等的概率.(注意特判询问有一个区间的左端点为1的情况,因为题 ...
随机推荐
- Django项目实战:解除跨域限制
Django项目实战:解除跨域限制 在Web开发中,跨域资源共享(CORS)是一个重要的安全特性,它限制了网页只能与其同源的服务器进行交互.然而,在开发过程中,我们经常需要前端(如Vue.js.Rea ...
- vue-element-admin改为从后台获取菜单
一.修改文件\src\router\index.js 文件的asyncRoutes清理为 export const asyncRoutes = [ { path: '*', redirect: '/4 ...
- LeetCode 题解大全
项目说明 LeetCode 系列题解致力于帮助程序员更好地理解和掌握算法思维,内容包含详细的解题思路分析.图文并茂的示例讲解和完整的代码实现. LeetCode 最全题解:持续更新中,欢迎 Star ...
- 无线路由器dBi越大越好吗?
无线路由器dBi越大越好吗? 目前,常见的无线路由器,通过查看参数可知,大多为3dBi.5dBi或7dBi,对于用户来说,这个数值到底是越大越好,还是越小越好呢?对于这个问题,其实通过下面这张天线增益 ...
- OpenLayers 4326地图 根据距离设置地图分辨率
问题:给一个距离,如何确定4326地图缩放等级或者分辨率使地图视口范围为这个距离 我本来打算计算从地图视口左上角到右下角的距离来计算地图缩放的,然后发现不好算,我也不会算,于是就计算水平方向距离的缩放 ...
- C# List应用 Lambda 表达式
参考链接 : https://blog.csdn.net/wori/article/details/113144580 首先 => 翻译为{ } 然后没有然后 主要基于我工作中常用的几种情况,写 ...
- 螺旋原型设计 (Spiral Model SDLC)
螺旋模型介绍 -:该模型描述了软件开发过程.该模型是两种模型的组合,首先是迭代模型和一个SDLC 模型,并将其与循环过程相结合. 该模型考虑了大多数其他模型通常没有注意到的风险.该模型从在一次迭代开始 ...
- 傻妞教程——对接QQ频道机器人
安装插件 前往傻妞插件市场安装QQ频道机器人插件,基于Node开发. 申请机器人 使用前请先确保已在机器人平台创建机器人 (opens new window),具体创建教程在超链接里面有,根据教程图以 ...
- 八米云-N1、机顶盒设置静态地址和PPPOE拨号流程
疑难解答加微信机器人,给它发:进群,会拉你进入八米交流群 机器人微信号:bamibot 简洁版教程访问:https://bbs.8miyun.cn 这里以老毛子路由系统举例: 一.设置静态地址 1.路 ...
- 湖北电信创维E900-S机顶盒-精简系统装当贝桌面
一.打开机顶盒进入本地配置,输入密码:6321,然后打开其他设置-管理应用程序,连续按遥控器方向右键5次左右,这时会出现[USB调试]并打开: 二.从电脑里下载好当贝市场(点击立即下载).当贝桌面(点 ...