关于我想了很久才想出这题咋做这档事 - 2
#0.0 包含题目
以下题目皆可点击查看原题
#1.0 P7074 [CSP-J2020] 方格取数
本题是CSP-J2020-T4,本人在考场上遇见时,竟没有推出转移方程,现在看来十分可惜qwq
#1.1 探索发现
初次看到这个题,先想到了如下一题:
“有一张有 \(n \times m\) 个方格的地图,每个方格上有一个数 \(d\),每次可以向下或向右走一格,求从左上角走到右上角所经格子上的数的和最大为多少。”
不必多讲,相信此题大家都能随手做出:我们只需要从右下角的方格 \((n,m)\) 向左上角 \((1,1)\) 倒推,方格 \((i,j)\) 的最大和为 \(f_{i,j} = \max\{f_{i+1,j},f_{i,j +1}\} + d_{i,j}\)
但是我们看这道题,每次可以走的格不再是只有两个,而是变为了三个,那要考虑的情况就变得多了,不能单单变成三个数中的最大值,那可能会出现明显的错误,比如以下这种情况:
我们看 \((5,3)\) 这个格子,它上面的数为 \(5\) ,如果按照上面的思路,应当比较 \((4,3),(5,4),(6,3)\) 这 \(3\) 个格子到达终点的最大值,显然, \((4,3)\) 这个格子的最大值路径为:
而 \((6,3)\) 这个格子的最大值路径为:
那么,根据上面的算法,会选择 \((6,3)\) 这个格子,但是,这样的路径就变成了:
可以看到,所经过的路径重复了,于是上面的算法显然是不可取的。
那么思考,上面的算法问题出现在了哪里?是没有考虑已选的路径对当前选择的影响。换句话说,就是做当前的选择与已选择的路径起了冲突,那冲突的来源是什么呢?是我们在做路径的选择时,抛弃了几个合适的选项。还是上面那个例子,如果从 \((5,3)\) 走 \((6,3)\) 这个格子,正确的路径应当是:
但是在做之前的选择时,\((6,3) \longrightarrow (6,4)\) 这个路径被我们丢弃了。
举个不恰当的例子,就像是有 \(3\) 个学生 小A、小B、小C,后来又来了一个 小D,我们要从小A、小B、小C三个人里找一个与 小D 形成搭档,已知小D与小B小C是好朋友,与小A是仇人,但是我们只看了小A、小B、小C三个人里谁成绩最好,选了小A,两个仇人见面,学习效果不必多讲...那防止这种事情发生的方案是什么呢?就是留下小B小C,先参照是否冲突,再按成绩选择。
那我们按上面的不恰当例子来想,就需要将每个格子 向 \(3\) 个方向走的最大值都存下来,以支持后续的计算。
#1.2 找寻递推方程
如下图:
显然,想要到达格子 \((i,j)\),最多只有 \(3\) 条路径,那从这 \(3\) 条路径到达点 \((i,j)\) 后,分别可以去向哪呢?如下图:
根据上面的探索及上面 \(3\) 张图,我们可以知道,我们需要将原本的 \(f_{i,j}\) 再拓展一维,变成 \(f_{i,j,k}\) ,其中 \(i,j\) 仍表示第 \(i\) 行第 \(j\) 个格子,\(k\) 表示从 \(k\) 方向进入该格子,那么 \(f_{i,j,k}\) 可表示从 \(k\) 方向进入 \((i,j)\) 可得到的最大值。
那么根据上面的三张图,不难找出计算规律:
- 从上方进入格子 \((i,j)\) ,我们要的最大值便是 从上方进入格子 \((i+1,j)\) 能得到的最大值 以及 从左方进入格子 \((i,j+1)\) 能得到的最大值 这两者中较大的一个加上格子 \((i,j)\) 本身的数
- 从左方进入格子 \((i,j)\) ,我们要的最大值便是 从上方进入格子 \((i+1,j)\) 能得到的最大值 以及 从左方进入格子 \((i,j+1)\) 能得到的最大值 以及 从下方进入格子 \((i-1,j)\) 能得到的最大值 这三者中较大的一个加上格子 \((i,j)\) 本身的数
- 从下方进入格子 \((i,j)\) ,我们要的最大值便是 从下方进入格子 \((i-1,j)\) 能得到的最大值 以及 从左方进入格子 \((i,j+1)\) 能得到的最大值 这两者中较大的一个加上格子 \((i,j)\) 本身的数
很容易写出状态转移方程:
那这样就解决了上面重复经过的问题了吗?
如从 \(1\) 方向进入(即从上方进入),我们会比较的只有在点格子\((i,j)\) 右方或下方的来源,不可能出现重复,其他方向同理。
#1.3 找寻边界,确定顺序
首先是最右下角的格 \((n,m)\) ,显然无论从任何方向进入它,得到的最大值都只能是 \(d_{n,m}\)
然后,考虑方格的边缘,比如这一列:
非常显然,这一列的格子无论是从上方进入还是从左方进入,要想到达终点,必须向下走,所以我们可以直接将 \(f_{i,m,3}(1\leqslant i< n)\) 的值赋为 \(-INF\) ,而 \(f_{i,m,1}\) 以及 \(f_{i,m,2}\) 的值都为 \(f_{i-1,m,1}+d_{i,m}\)。
找到这一行的关系,我们再来找运算顺序,来看这一列:
如果要计算 \(f_{i,m-1,1}\),那需要知道 \(f_{i+1,m-1,1}\) 和 \(f_{i,m,2}\),根据上面的初始化, \(f_{i,m,2}\) 是已知的,所以我们只需要知道 \(f_{i+1,m-1,1}\),就需要知道 \(f_{i+2,m-1,1}\),\(\cdots\),以此类推,我们最终需要知道 \(f_{n,m-1,1}\),显然它只能向右走一格,那么这一列 \(f_{i,m-1,1}\) 的值便都可求了。所以我们便能确定,\(f_{i,m-1,1}\) 的求值顺序为自下而上。
再看 \(f_{i,m-1,2}\),那需要知道 \(f_{i-1,m-1,3},f_{i+1,m-1,1}\) 和 \(f_{i,m,2}\),但是 \(f_{i-1,m-1,3}\) 的值未知,所以先放一放。
来看 \(f_{i,m-1,3}\),那需要知道 \(f_{i-1,m-1,3}\) 和 \(f_{i,m,2}\),所以我们只需要知道 \(f_{i-1,m-1,3}\),就需要知道 \(f_{i-2,m-1,3}\),\(\cdots\),以此类推,我们最终需要知道 \(f_{1,m-1,3}\),显然它只能向右走,那么这一列 \(f_{i,m-1,3}\) 的值便都可求了。所以我们便能确定,\(f_{i,m-1,3}\) 的求值顺序为自上而下。同时,我们看,再求 \(f_{i,m-1,3}\) 时,\(f_{i-1,m-1,3}\) 是已知的,此时便可以同时求 \(f_{i,m-1,2}\)。
不难发现,求 \(f_{i,m-2,k}\) 时是一样的思路,所以我们便能确定最终的顺序:
- 整体自右而左
- \(f_{i,j,1}\) 自下而上
- \(f_{i,j,2}\) 和 \(f_{i,j,3}\) 自上而下同时求
以上,要得出代码就不难了。
#1.4 撸代码
其实撸代码不一定上来就要最终结果,可以循序渐进的来,就像这道题,我们可以以 深搜dfs -> 记忆化 -> 递推 的顺序实现。
#1.4.1 DFS
DFS思路很简单,不再赘述。
/* 这是本人在考场上写的lj代码...甚至连记忆化都没有...*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 1010
#define INF 0x3fffffff
#define ll long long
using namespace std;
int n,m;
int ma[N][N],vis[N][N],ans = -INF;
int f[N][N][3];
int to[3][2] = {{-1,0},{0,1},{1,0}}; //深搜时不同的方向
inline void dfs(int x,int y,int s){ //深搜主代码
if (x == n && y == m){ //到地方了,比较结果
s += ma[x][y];
ans = max(ans,s);
return;
}
vis[x][y] = 1; //标记
for (int i = 0;i <= 2;i ++){
int cx = x + to[i][0];
int cy = y + to[i][1];
if (cx <= n && cx >= 1 && cy <= m && cy >= 1 && (!vis[cx][cy])){
dfs(cx,cy,s + ma[x][y]);
}
}
vis[x][y] = 0; //记得回溯
}
int main(){
scanf("%d%d",&n,&m);
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= m;j ++)
scanf("%d",&ma[i][j]);
dfs(1,1,0);
cout << ans;
return 0;
}
#1.4.2 记忆化搜索
仅仅是将下面的递推换一种写法,不过多赘述(我懒得写)
#1.4.3 递推
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define INF 0x3fffffff
#define int long long
#define N 1010
#define mset(s,l) memeset(s,l,sizeof(s))
using namespace std;
int n,m;
int d[N][N],f[N][N][3];
signed main(){
scanf("%lld%lld",&n,&m);
for (int i = 1;i <= n;i ++)
for (int j = 1;j <= m;j ++)
scanf("%lld",&d[i][j]);
for (int i = 1;i <= m;i ++){ //第一次预处理
f[0][i][1] = f[0][i][2] = f[0][i][3] = -INF; //防止计算时出现走出格子的情况
f[n + 1][i][1] = f[n + 1][i][2] = f[n + 1][i][3] = -INF; //同上
f[1][i][1] = -INF;
f[n][i][3] = -INF;
}
f[n][m][1] = f[n][m][2] = f[n][m][3] = d[n][m]; //终点
for (int i = n - 1;i >= 1;i --){ //再一次预处理
f[i][m][1] = f[i + 1][m][1] + d[i][m]; //处理最右边的一列
f[i][m][2] = f[i + 1][m][1] + d[i][m];
f[i][m][3] = -INF;
}
for (int i = m - 1;i >= 1;i --){ //按照状态转移方程进行计算
if (i == 1){ //特判一下,在最左列时,不存在从下方或左方进入的情况
for (int j = n;j >= 1;j --){
f[j][i][2] = -INF;
f[j][i][3] = -INF;
f[j][i][1] = max(f[j + 1][i][1],f[j][i + 1][2]) + d[j][i];
}
}
for (int j = n;j >= 1;j --)
f[j][i][1] = max(f[j + 1][i][1],f[j][i + 1][2]) + d[j][i];
for (int j = 1;j <= n;j ++){
f[j][i][3] = max(f[j - 1][i][3],f[j][i + 1][2]) + d[j][i];
f[j][i][2] = max(f[j - 1][i][3],max(f[j + 1][i][1],f[j][i + 1][2])) + d[j][i];
}
}
printf("%lld",f[1][1][1]); //输出,结束
return 0;
}
#2.0 P3842 [TJOI2007]线段
题意很容易理解,是一道不难的dp
#2.1 寻找状态
这道题主要是要找到合适的状态进行转移,简单的举几个例子,画一画图就能得到,每一次来到下一行时,只存在两种状态:
- 从上一条线段左端点下来
- 从上一条线段右端点下来
(至于从本行线段的某个起点在上一行的对应处下来的情况,与以上两种情况本质相同,简单平移即可得到,无需分类)
那我们可以这样设置状态:设 \(f_{i,k}\) 为以第 \(i\) 行的线段的左端点 (\(k=0\))或右端点(\(k = 1\))为结束点时所经过的最短路程
#2.2 写转移方程
那我们来考虑,怎么转移呢?换句话说,就是怎么从上一个结束点来到当前结束点,我们看下图:
以 \((i,j)\) 为结束点,无非就这两种情况,\((i,j)\) 要么是左端点,要么是右端点,那以下两种情况:
- 终点为右端点,两种情况:
- 从上一线段的左端点下来,来到本线段左端点,再走遍整条线段到右端点
- 整个距离为上一线段左端点到本线段左端点的距离加本线段长度
- 从上一线段的右端点下来,来到本线段左端点,再走遍整条线段到右端点
- 整个距离为上一线段右端点到本线段左端点的距离加本线段长度
- 那我们要的结果无非是上面两种情况各加上以上一线段相应端点为终点的最短距离中最短的一个
- 从上一线段的左端点下来,来到本线段左端点,再走遍整条线段到右端点
- 终点为左端点,两种情况:
- 从上一线段的左端点下来,来到本线段右端点,再走遍整条线段到左端点
- 整个距离为上一线段左端点到本线段右端点的距离加本线段长度
- 从上一线段的右端点下来,来到本线段右端点,再走遍整条线段到左端点
- 整个距离为上一线段右端点到本线段右端点的距离加本线段长度
- 那我们要的结果无非是上面两种情况各加上以上一线段相应端点为终点的最短距离中最短的一个
- 从上一线段的左端点下来,来到本线段右端点,再走遍整条线段到左端点
以上,怎样转移便讨论完了,只剩动手撸代码
#2.3 撸代码
代码很简单,不过要注意对 “距离” 的处理,自定义一个abs()
函数进行绝对值运算即可。
#include <iostream>
#include <cstdio>
#include <cstring>
#define N 20004
using namespace std;
struct Line{ //每天线段的信息
int l; //左端点
int r; //右端点
int length; //长度
};
Line l[N];
int n,f[N][2];
inline int abs_(int x){ //abs_函数用于取绝对值计算距离
return (x >= 0) ? x : -x;
}
int main(){
scanf("%d",&n);
for (int i = 1;i <= n;i ++){
scanf("%d%d",&l[i].l,&l[i].r);
l[i].length = l[i].r - l[i].l;
}
f[1][0] = l[1].r - 1 + l[1].length; //第一条线段单独计算
f[1][1] = l[1].r - 1;
for (int i = 2;i <= n;i ++){ //按转移的思路进行转移
int l1 = 1 + abs_(l[i - 1].l - l[i].r) + l[i].length + f[i - 1][0];
int l2 = 1 + abs_(l[i - 1].r - l[i].r) + l[i].length + f[i - 1][1];
f[i][0] = min(l1,l2);
l1 = 1 + abs_(l[i - 1].l - l[i].l) + l[i].length + f[i - 1][0];
l2 = 1 + abs_(l[i - 1].r - l[i].l) + l[i].length + f[i - 1][1];
f[i][1] = min(l1,l2);
}
printf("%d",min(n - l[n].l + f[n][0],n - l[n].r + f[n][1])); //注意最后求得的不是最后答案,要回到(n,m)点才可
return 0;
}
#3.0P2661 [NOIP2015 提高组] 信息传递
运用并查集求最小环。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
#define N 200010
#define INF 0x3fffffff
#define mset(l,x) memset(l,x,sizeof(l))
#define mp(a,b) make_pair(a,b)
using namespace std;
int n,t[N],fa[N],ans,l[N];
inline int find(int x){ //找爹同时记录长度
if (x != fa[x]){
int last = fa[x];
fa[x] = find(fa[x]);
l[x] += l[last];
}
return fa[x];
}
inline void check(int a,int b){ //是不是出现了环
int afa = find(a);
int bfa = find(b);
if (afa == bfa)
ans = min(ans,l[a] + l[b] + 1); //是,更新答案
else{
fa[afa] = bfa; //不是,合并,加长
l[a] = l[b] + 1;
}
return;
}
int main(){
scanf("%d",&n);
ans = INF;
for (int i = 1;i <= n;i ++)
fa[i] = i;
for (int i = 1;i <= n;i ++){
int x;
scanf("%d",&x);
check(i,x);
}
printf("%d",ans);
return 0;
}
更新日志及说明
更新
- 初次完成编辑 - \(2021.1.25\)