这是本蒟蒻刷动态规划专题的题目时,刷到的题单,题单在洛谷,算是简单动态规划入门题。在此献给大家。阿门!
简单DP,递推公式很容易找到。dp[i][j] 表示到达数字三角形第
i
i
i行的第
j
j
j个数的最大数字和。
其实只需要开一维数组即可,边输入边计算结果。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f;
int tria[N][N]; //存数字三角形
int main()
{
int r;
cin >> r;
for(int i = 1; i <= r; i ++)
for(int j = 1; j <= i; j ++)
cin >> tria[i][j];
for(int i = 1; i <= r; i ++)
for(int j = 1; j <= r; j ++)
tria[i][j] += max(tria[i - 1][j - 1], tria[i - 1][j]); //递推公式
int ans = - INF;
for(int i = 1; i <= r; i ++)
ans = max(ans, tria[r][i]);
cout << ans << endl;
return 0;
}
用深度优先搜索+记忆化搜索。深度优先搜索搜出来的结果用dis[i][j] 来表示,意为以点
(
i
,
j
)
(i, j)
(i,j)为最高峰的最长距离。所有点都搜一遍,已经搜过的点的结果存入数组
d
i
s
dis
dis中。
很明显,因为低的不可能滑向高的,所以我们不需要再开一个数组去记录这个点是否走过。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110;
int row, col, ski[N][N], dis[N][N], dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0}; //ski记录滑雪场的高度,dis记录以每个点为最高峰的最长距离
int dfs(int n, int m) //深度优先搜索,记忆化搜索
{
if(dis[n][m]) //算过的直接返回,因为算过的最长距离至少为1,不会为0
return dis[n][m];
for(int i = 0; i < 4; i ++)
{
int x = n + dx[i], y = m + dy[i];
if(0 < x && x <= row && 0 < y && y <= col && ski[n][m] > ski[x][y]) //四个方向和高度判断
dis[n][m] = max(dis[n][m], dfs(x, y)); //取最大值
}
dis[n][m] ++; //还要加上自己本身的长度
return dis[n][m];
}
int main()
{
cin >> row >> col;
for(int i = 1; i <= row; i ++)
for(int j = 1; j <= col; j ++)
cin >> ski[i][j];
int ans = 0; //初始化最终结果
for(int i = 1; i <= row; i ++)
for(int j = 1; j <= col; j ++)
if(ans < dfs(i, j)) //取所有点中的最大值
ans = dis[i][j]; //前面已经算了一遍了,用数组存下了,这里可以直接用;写成dfs(i, j)也行
cout << ans << endl;
return 0;
}
对了,这题贼坑,图是有向图,不是无向图。其实这题,最后一个地窖只能被连接,不能连接别人。
用深度优先搜索即可。遍历每个点,以每个点为起点进行dfs。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 25;
int n, mine[N], graph[N][N], path[N], ans[N], cnt, maxn; //mine记录地雷,graph记录图的边,即两个地雷之间的联系,path记录以每个点为起点的路径,ans是最终记录的路径,cnt记录最终挖了多少个地窖,maxn记录最终挖了多少个地雷
void dfs(int x, int step, int sum) //x记录现在位置,step记录走了几个点,sum记录挖的地雷总数
{
int sign = 0; //记录是不是已经是叶子结点,即后面无结点
for(int i = x + 1; i <= n; i ++)
if(graph[x][i])
{
path[step + 1] = i; //不要写++ step,这里的step值不能变
dfs(i, step + 1, sum + mine[i]);
sign = 1; //说明还有结点
}
if(!sign)
{
if(maxn < sum) //交换地雷数、地窖数和路径
{
maxn = sum;
cnt = step;
for(int i = 1; i <= cnt; i ++)
ans[i] = path[i];
}
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> mine[i];
for(int i = 1; i < n; i ++)
for(int j = i + 1; j <= n; j ++)
cin >> graph[i][j];
for(int i = 1; i <= n; i ++) //以每个点为起点开始dfs
{
path[1] = i; //记录起点
dfs(i, 1, mine[i]);
}
for(int i = 1; i <= cnt; i ++)
cout << ans[i] << " ";
cout << endl << maxn << endl;
return 0;
}
dp的做法。用一维dp[i] 表示以
i
i
i为起点可以挖到的最多的地雷数,但是得倒序遍历。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 25;
int n, mine[N], graph[N][N], pre[N], dp[N]; //mine记录地雷,graph记录图的边,即两个地雷之间的联系,pre[i]记录每个i所指向的下一个儿子,这个儿子使得它的地雷数最多
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> mine[i];
for(int i = 1; i < n; i ++)
for(int j = i + 1; j <= n; j ++)
cin >> graph[i][j];
dp[n] = mine[n]; //初始化
for(int i = n - 1; i > 0; i --) //倒序遍历每一点
{
for(int j = i + 1; j <= n; j ++) //遍历所有儿子
if(graph[i][j])
{
if(dp[i] < dp[j]) //选择地雷数最多的儿子,并记录下来
{
dp[i] = dp[j];
pre[i] = j;
}
}
dp[i] += mine[i]; //记得加上自身的地雷数
}
int ans = 0, start; //ans记录最大地雷数,start记录ans对应的起点
for(int i = 1; i <= n; i ++)
if(ans < dp[i])
{
ans = dp[i];
start = i;
}
for(int i = start; i; i = pre[i]) //当i为0时,表示到头了,因为pre初始化就为0
cout << i << " ";
cout << endl << ans << endl;
return 0;
}
这道题贼坑,不要光看样例,原本以为是按顺序的边的有向边,即1没有入度,最后一个数没有出度,事实证明我错了。这个跟结点编号没有关系,任何一个结点都有可能有出度和入度。
这道题还得拓扑排序。这里用了两种队列,其实差不多,思路都一样。用两个数组分别记录每个结点的入度和出度。入度为0的点入队列,出度为0的点计入结果。拓扑排序其实就是一种宽度优先搜索。
同时还要dp,用dp[i] 表示从所有入度为0的结点出发到结点
i
i
i的食物链数目。
如何更新dp[i] ?在遍历每个入度为0的结点的时候,我们把其指向的所有结点都更新一遍。递推公式为:dp[j] = (dp[i] + dp[j]) % mod (i -> j成立)。
手动写的队列:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 5e3 + 10, M = 5e5 + 10, mod = 80112002; //这里一定要记得N和M不一样,都要定义
int e[M], ne[M], h[N], idx; //三个数组的范围不一样!!!
int indeg[N], q[N], dp[N], outdeg[N];
int hh, tt = -1;
int main()
{
memset(h, -1, sizeof h); //记得要初始化
int n, m;
cin >> n >> m;
while(m --)
{
int a, b;
cin >> a >> b;
indeg[b] ++;
outdeg[a] = 1;
ne[idx] = h[a], h[a] = idx, e[idx ++] = b; //一套组合操作
}
for(int i = 1; i <= n; i ++)
if(!indeg[i])
{
q[++ tt] = i; //入队
dp[i] = 1; //记得赋初值
}
int ans = 0;
while(hh <= tt) //不为空队列
{
int t = q[hh]; //取出队头
for(int i = h[t]; i != -1; i = ne[i]) //遍历以队头结点为入点的所有边
{
int val = e[i]; //得到出点
indeg[val] --; //入度减一
if(!indeg[val])
q[++ tt] = val; //进队
dp[val] = (dp[val] + dp[t]) % mod; //及时更新
}
if(!outdeg[t]) //出度为0,计入结果
ans = (ans + dp[t]) % mod;
hh ++; //已遍历的结点丢掉
}
cout << ans << endl;
/*调试代码
for(int i = 1; i <= n; i ++)
cout << dp[i] << " ";
*/
return 0;
}
用STL中的queue:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 5e3 + 10, M = 5e5 + 10, mod = 80112002; //这里一定要记得N和M不一样,都要定义
int e[M], ne[M], h[N], idx; //三个数组的范围不一样!!!
int indeg[N], dp[N], outdeg[N];
queue<int> q;
int main()
{
memset(h, -1, sizeof h);
int n, m;
cin >> n >> m;
while(m --)
{
int a, b;
cin >> a >> b;
indeg[b] ++;
outdeg[a] = 1;
ne[idx] = h[a], h[a] = idx, e[idx ++] = b;
}
for(int i = 1; i <= n; i ++)
if(!indeg[i])
{
q.push(i);
dp[i] = 1;
}
int ans = 0;
while(!q.empty())
{
int t = q.front();
for(int i = h[t]; i != -1; i = ne[i])
{
int val = e[i];
indeg[val] --;
if(!indeg[val])
q.push(val);
dp[val] = (dp[val] + dp[t]) % mod;
}
if(!outdeg[t])
ans = (ans + dp[t]) % mod;
q.pop();
}
cout << ans << endl;
/*调试代码
for(int i = 1; i <= n; i ++)
cout << dp[i] << " ";
*/
return 0;
}
DFS(深度优先搜索)的做法:
注意,这里必须要记忆化搜索,否则就会TLE。
遍历所有入度为0的结点,深搜出以其为起点的所有最大食物链数目,累加取模得结果。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 5e3 + 10, M = 5e5 + 10, mod = 80112002; //这里一定要记得N和M不一样,都要定义
int e[M], ne[M], h[N], idx; //三个数组的范围不一样!!!
int indeg[N], outdeg[N], paths[N]; //paths[i]记录从结点i出发到所有出度为0的结点的路径数
int dfs(int x) //从结点x出发到所有出度为0的结点的路径数
{
if(!outdeg[x]) //到了最后一个结点,直接返回
return 1;
if(paths[x]) //记忆化搜索
return paths[x];
int sum = 0;
for(int i = h[x]; i != -1; i = ne[i]) //遍历所有入点
sum = (sum + dfs(e[i])) % mod;
return paths[x] = sum;
}
int main()
{
memset(h, -1, sizeof h);
int n, m;
cin >> n >> m;
while(m --)
{
int a, b;
cin >> a >> b;
indeg[b] = 1;
outdeg[a] = 1;
ne[idx] = h[a], h[a] = idx, e[idx ++] = b; //一套组合操作
}
int ans = 0;
for(int i = 1; i <= n; i ++)
if(!indeg[i]) //遍历所有入度为0的结点,深搜出以其为起点的所有最大食物链数目,累加取模得结果
ans = (ans + dfs(i)) % mod;
cout << ans << endl;
return 0;
}
01背包模板题。注意要初始化,以及倒序遍历即可。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e2 + 10, M = 1e3 + 10;
int main()
{
int t, m; //t记录总的时间,m记录药材的种数
cin >> t >> m;
int time[N], val[N]; //分别记录采药材的所需时间,药材的价值
for(int i = 1; i <= m; i ++)
cin >> time[i] >> val[i];
int dp[M];
memset(dp, 0, sizeof dp); //初始化
for(int i = 1; i <= m; i ++)
for(int j = t; j >= time[i]; j --) //记得要倒序遍历
dp[j] = max(dp[j], dp[j - time[i]] + val[i]); //递推方程
cout << dp[t] << endl;
return 0;
}
DFS+记忆化搜索做法:
参考大佬的笔记:P1048 [NOIP2005 普及组] 采药 - 洛谷
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e2 + 10, M = 1e3 + 10;
int t, m; //t记录总的时间,m记录药材的种数
int tm[N], val[N]; //分别记录采药材的所需时间,药材的价值
int dp[N][M];
int dfs(int x, int remt) //剩余:remain/remainder //正在考虑第x个物品,还剩remt个空间
{
if(x == m + 1)
return 0;
if(dp[x][remt] != -1)
return dp[x][remt];
int ans1, ans2 = 0; //分为两种情况:不选或选;可能有空间却选不了,故初始化为0
ans1 = dfs(x + 1, remt); //不选,直接看下一个物品
if(remt >= tm[x])
ans2 = dfs(x + 1, remt - tm[x]) + val[x]; //选
return dp[x][remt] = max(ans1, ans2);
}
int main()
{
memset(dp, -1, sizeof dp);
cin >> t >> m;
for(int i = 1; i <= m; i ++)
cin >> tm[i] >> val[i];
cout << dfs(1, t) << endl;
return 0;
}
完全背包模板题。注意要顺序枚举容量即可,还有本题要注意数据范围,防止MLE,还要开long long型数据。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e4 + 10, M = 1e7 + 10;
int main()
{
int t, m;
cin >> t >> m;
int tm[N], val[N];
for(int i = 1; i <= m; i ++)
cin >> tm[i] >> val[i];
LL dp[M];
memset(dp, 0, sizeof dp);
for(int i = 1; i <= m; i ++)
for(int j = tm[i]; j <= t; j ++) //顺序枚举容量
dp[j] = max(dp[j], dp[j - tm[i]] + val[i]);
cout << dp[t] << endl;
return 0;
}
受到上一题的启发,这里用记忆化搜索试了一下,最高可以到80分,也就是说当
N
?
M
N * M
N?M太大时,会MLE。故得看情况使用。
这种背包dp,深搜的顺序不要紧,但有的题会有关系。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e4 + 10, M = 1e7 + 10;
int t, m; //t记录总的时间,m记录药材的种数
int tm[N], val[N]; //分别记录采药材的所需时间,药材的价值
int dp[N][M];
int dfs(int x, int remt) //剩余:remain/remainder //正在考虑第x个物品,还剩remt个空间
{
if(x == m + 1)
return 0;
if(dp[x][remt] != -1)
return dp[x][remt];
int ans1, ans2 = 0; //分为两种情况:不选或选;可能有空间却选不了,故初始化为0
ans1 = dfs(x + 1, remt); //不选,直接看下一个物品
for(int i = 1; remt >= i * tm[x]; i ++)
ans2 = max(ans2, dfs(x + 1, remt - i * tm[x]) + i * val[x]);
/*
if(remt >= tm[x])
ans2 = dfs(x + 1, remt - tm[x]) + val[x]; //选*/
return dp[x][remt] = max(ans1, ans2);
}
int main()
{
memset(dp, -1, sizeof dp);
cin >> t >> m;
for(int i = 1; i <= m; i ++)
cin >> tm[i] >> val[i];
cout << dfs(1, t) << endl;
return 0;
}
01背包模板题。这里有一维做法,二维做法以及记忆化搜索法。题目数据有错,记得开long long。
二维:理解题意,不选也有值可以加,建立正确的状态转移方程。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10;
LL dp[N][N];
int main()
{
int n, x;
cin >> n >> x;
int lose[N], win[N], use[N];
for(int i = 1; i <= n; i ++)
cin >> lose[i] >> win[i] >> use[i];
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= x; j ++) //这里和二维不一样
{
dp[i][j] = dp[i - 1][j] + lose[i]; //先初始化
if(use[i] <= j) //符合条件才更新
dp[i][j] = max(dp[i][j], dp[i - 1][j - use[i]] + win[i]);
}
cout << 5 * dp[n][x] << endl;
return 0;
}
一维滚动:
首先,呈递一个错误代码,得40分。具体原因是,数据中use可能为0。当用二维数组表示时就不会出错;用一维滚动数组时就会出错。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10;
LL dp[N];
int main()
{
int n, x;
cin >> n >> x;
int lose[N], win[N], use[N];
for(int i = 1; i <= n; i ++)
cin >> lose[i] >> win[i] >> use[i];
for(int i = 1; i <= n; i ++)
for(int j = x; j >= 0; j --) //这里和一维不一样
{
dp[j] += lose[i];
if(use[i] <= j)
dp[j] = max(dp[j], dp[j - use[i]] + win[i]); //数据有陷阱,use可能为0,则不满足下标递减的规律
}
cout << 5 * dp[x] << endl;
return 0;
}
正确代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10;
LL dp[N];
int main()
{
int n, x;
cin >> n >> x;
int lose[N], win[N], use[N];
for(int i = 1; i <= n; i ++)
cin >> lose[i] >> win[i] >> use[i];
for(int i = 1; i <= n; i ++) //这里和一维不一样
{
int j = x; //把它们分成两部分就不怕了
for(j = x; j >= use[i]; j --)
dp[j] = max(dp[j] + lose[i], dp[j - use[i]] + win[i]);
for(; j >= 0; j --)
dp[j] += lose[i];
}
cout << 5 * dp[x] << endl;
return 0;
}
记忆化搜索法:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10;
int n, x;
int lose[N], win[N], use[N];
LL dp[N][N];
LL dfs(int pos, int remx) //剩余的x;正在考虑第pos个位置的物品,还剩remx个空间
{
if(pos == n + 1)
return 0;
if(dp[pos][remx] != -1)
return dp[pos][remx];
LL ans1, ans2 = 0; //分三种情况,不能选,能选但不选,能选且选
ans1 = dfs(pos + 1, remx) + lose[pos]; //ans1先记录不选或者选不了的情况,后面可以对最终结果进行更改
if(use[pos] <= remx) //如果可以选
ans2 = max(ans1, dfs(pos + 1, remx - use[pos]) + win[pos]); //分两种情况
return dp[pos][remx] = max(ans1, ans2);
}
int main()
{
cin >> n >> x;
for(int i = 1; i <= n; i ++)
cin >> lose[i] >> win[i] >> use[i];
memset(dp, -1, sizeof dp);
cout << 5 * dfs(1, x) << endl;
return 0;
}
二维dp做法:
如果不考虑马的拦截,那么就有递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 成立。dp[i][j] 表示从点(0, 0)到点(i, j)路径数,那么它等于原点到它左边的点的路径数+原点到它上面的点的路径数。
现在考虑马的拦截,我们让马的拦截点的dp值始终为0(因为它这个点不可到达),这样就不会影响别的点的dp值了。上述递推公式可以照用。所以在dp之前应该预先算出马的拦截点,并记录下来,这里用flag来记录,其值为1,其余可到达的点为0。然后在dp之前,初始化。从小到大一层一层递推,每一层从左到右,遇见马的拦截点,直接跳过。对了,我还让坐标从1开始,这样有利于递推。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 25;
LL dp[N][N], flag[N][N];
int dx[9] = {-2, -2, -1, -1, 0, 1, 1, 2, 2}, dy[9] = {-1, 1, -2, 2, 0, -2, 2, -1, 1};
//一头马对应着九个位置,这里是两个坐标轴的变化;flag记录马的拦截点,为1,其余为0。
int main()
{
int n, m, hsx, hsy;
cin >> n >> m >> hsx >> hsy;
for(int i = 0; i < 9; i ++) //事先算出马的拦截点
if(0 < hsx + 1 + dx[i] && 0 < hsy + 1 + dy[i])
flag[hsx + 1 + dx[i]][hsy + 1 + dy[i]] = 1;
dp[1][1] = 1; //初始化
for(int i = 1; i <= n + 1; i ++)
for(int j = 1; j <= m + 1; j ++)
if(!flag[i][j]) //遇见拦截点就走,保证拦截点的dp值为0
dp[i][j] += dp[i - 1][j] + dp[i][j - 1]; //这里的+=主要对点(1, 1)起作用
cout << dp[n + 1][m + 1] << endl; //下标在题意的基础上加一
return 0;
}
记忆化搜索法:
受到大佬的影响,反正以后dp能解决的问题,我一定要用DFS+记忆化搜索试试。
不过要注意,题目的原点是(0, 0),终点是(n, m);我这里的原点是(1, 1),终点是(n + 1, m + 1)。
dp[i][j] 的含义和上述相同,递推公式也同上。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 25;
LL dp[N][N];
int flag[N][N];
int dx[9] = {-2, -2, -1, -1, 0, 1, 1, 2, 2}, dy[9] = {-1, 1, -2, 2, 0, -2, 2, -1, 1};
//一头马对应着九个位置,这里是两个坐标轴的变化;flag记录马的拦截点,为1,其余为0。
LL dfs(int x, int y)
{
if(dp[x][y] != -1) //记忆化搜索,直接返回
return dp[x][y];
if(flag[x][y] || !x || !y) //马的拦截点和超出范围的点的dp值均为0
return dp[x][y] = 0;
return dp[x][y] = dfs(x - 1, y) + dfs(x, y - 1); //递推公式
}
int main()
{
int n, m, hsx, hsy;
cin >> n >> m >> hsx >> hsy;
for(int i = 0; i < 9; i ++) //事先算出马的拦截点
if(0 < hsx + 1 + dx[i] && 0 < hsy + 1 + dy[i])
flag[hsx + 1 + dx[i]][hsy + 1 + dy[i]] = 1;
memset(dp, -1, sizeof dp);
dp[1][1] = 1; //初始化
cout << dfs(n + 1, m + 1) << endl;
return 0;
}
|