【每日一题】【前缀和优化】【前/后缀最值】牛客练习赛139 B/C题 大卫的密码 (Hard Version) C++
牛客练习赛139 B题 大卫的密码 (Easy Version)
牛客练习赛139 C题 大卫的密码 (Hard Version)
大卫的密码
题目背景
牛客练习赛139
题目描述
给定一个 n × m n\times m n×m的网格图,我们使用 ( i , j ) (i,j) (i,j)表示网格中从上往下数第 i i i行和从左往右数第 j j j列的单元格。左上角为 ( 1 , 1 ) (1,1) (1,1),右下角为 ( n , m ) (n,m) (n,m),每个格子包含一个整数价值,使用 a i , j a_{i,j} ai,j表示。
一个光标在上面移动,从 ( s , 1 ) (s,1) (s,1)出发,每次可以向右或者向下移动,每个格子至多经过一次。当光标移动到 ( n , i ) ( 1 ≤ i ≤ m ) (n,i)(1\le i \le m) (n,i)(1≤i≤m)格子,也就是最后一行的某个格子时,继续向下移动则会到达 ( 1 , i ) (1,i) (1,i)格子。大卫需要移动光标到达 ( t , m ) (t,m) (t,m),求最终大卫能获取的最大价值和。
注意本题的时间限制。本题数据量较大,我们建议您选取较快的读入方式。
输入格式
第一行输入四个整数 n , m , s , t ( 1 ≤ n , m ≤ 2 × 10 3 ; 1 ≤ s , t ≤ n ) n, m,s,t(1\le n ,m \le 2\times10^3;1\le s,t \le n) n,m,s,t(1≤n,m≤2×103;1≤s,t≤n)
注:简单版本的范围是 : 1 ≤ n , m ≤ 500 1\le n ,m \le 500 1≤n,m≤500
此后 n n n 行,第 i i i 行输入 m m m个整数 a i , 1 , a i , 2 , . . . , a i , m ( − 5 × 10 4 ≤ a i , j ≤ 5 × 10 4 ) a_{i,1},a_{i,2},...,a_{i,m}(-5\times 10^4\le a_{i,j} \le 5\times 10^4) ai,1,ai,2,...,ai,m(−5×104≤ai,j≤5×104),表示一个格子的价值
输出格式
在一行上输出一个整数,表示大卫能获取的最大价值和。
样例 #1
样例输入 #1
3 3 1 2
1 2 3
4 5 6
7 8 9
样例输出 #1
38
说明
小红失去向上走的能力,消除 ( 1 , 3 ) (1,3) (1,3)处障碍,从起点到终点的最小步数为 6 6 6。
样例 #2
样例输入 #2
5 5 1 5
6 10 4 10 6
-6 6 10 3 -1
-10 9 -6 -10 10
-3 8 4 5 6
-5 -8 2 4 8
样例输出 #2
100
做题要点
- 每次可以向右或者向下移动(说明不能往回走)
- 每个格子至多经过一次
- 时间给的是2s
- 到某一列的最后一行在往下就到该列的第一行
做题思路
先考虑简单版本
首先因为 1 ≤ n , m ≤ 500 1\le n ,m \le 500 1≤n,m≤500,所以哪怕是 O ( n 3 ) / O ( m × n 2 ) / O ( n × m 2 ) O(n^3) / O(m \times n^2) / O(n \times m^2) O(n3)/O(m×n2)/O(n×m2)都可以考虑
逆向思考
这里设 d p i , j dp_{i,j} dpi,j表示从起点走到 ( i , j ) (i,j) (i,j)点的大卫能获取的最大价值和。
从某一点 ( i , j ) (i,j) (i,j)考虑,那么答案一定是
d p i , j = { max ( d p i , j − 1 , d p i − 1 , j ) + a i , j , ( i ≠ s , j ≠ 1 ) d p i , j = a i , j , ( i = s , j = 1 ) dp_{i,j} = \begin{cases} \max{(dp_{i,j-1},dp_{i-1,j})} + a_{i,j} , (i\neq s , j \neq 1) \\ dp_{i,j} = a_{i,j} ,(i = s , j = 1)\end{cases} dpi,j={max(dpi,j−1,dpi−1,j)+ai,j,(i=s,j=1)dpi,j=ai,j,(i=s,j=1)
加上记忆化搜索后dfs代码:
int dfs(int i,int j,int k){if(vis[i][j])return dp[i][j];vis[i][j] = true;if(i == s && j == 1)return dp[i][j] = a[i][j];int k1=-inf,k2=-inf;if(j != 1)k1=dfs(i,j_f(j),0);if(k < n-1)k2 = max(k2,dfs(i_f(i),j,k+1));return dp[i][j]=max(k1,k2) + a[i][j];
}
但是注意到某一列的最后一行在往下就到该列的第一行,所以就会出现一个环,导致递推式无法完全计算。
注意: d p i , j = max ( d p i , j − 1 , d p i − 1 , j ) + a i , j dp_{i,j} = \max{(dp_{i,j-1},dp_{i-1,j})} + a_{i,j} dpi,j=max(dpi,j−1,dpi−1,j)+ai,j
那么 d p i , j dp_{i,j} dpi,j的答案依赖于 d p i , j − 1 dp_{i,j-1} dpi,j−1和 d p i − 1 , j dp_{i-1,j} dpi−1,j,如果只关注后面的那个依赖关系
那么就会有
d p 2 , j dp_{2,j} dp2,j依赖于 d p 1 , j dp_{1,j} dp1,j;
d p 1 , j dp_{1,j} dp1,j依赖于 d p m , j dp_{m,j} dpm,j;
d p m , j dp_{m,j} dpm,j依赖于 d p m − 1 , j dp_{m-1,j} dpm−1,j
d p m − 1 , j dp_{m-1,j} dpm−1,j依赖于 d p m − 2 , j dp_{m-2,j} dpm−2,j
…
d p 3 , j dp_{3,j} dp3,j依赖于 d p 2 , j dp_{2,j} dp2,j
最后就会发现 d p i , j dp_{i,j} dpi,j依赖于自己 d p i , j dp_{i,j} dpi,j( j ≠ 1 j\neq 1 j=1)。如果依赖符号用有向箭头连接成一个有向图,就会发现是一个环。所以此写法欠缺考虑。
注:如果不考虑记忆化搜索优化,会TLE
int dfs(int i,int j,int k){if(i == s && j == 1)return a[i][j];int k1=-inf,k2=-inf;if(j != 1)k1=dfs(i,j_f(j),0);if(k < n-1)k2 = max(k2,dfs(i_f(i),j,k+1));return max(k1,k2) + a[i][j];
}
正向思考
但刚刚的逆向思考中,会发现除了第一列的 d p i , 1 dp_{i,1} dpi,1都得到了答案,其他都是未知的或错解。
其根本原因是 ( s , 1 ) (s,1) (s,1)的点是起点, d p s , 1 dp_{s,1} dps,1不依赖于(其他)未知的量,更不依赖于本身。
至此第一列的答案都是可以由 d p s , 1 dp_{s,1} dps,1推出的定值
那假设从第一行第一列往第一行第二列走。并且设其 d p 1 , 2 = d p 1 , 1 + a 1 , 2 dp_{1,2} = dp_{1,1} + a_{1,2} dp1,2=dp1,1+a1,2。这样一来就达到了破环的效果,再用第一列的方法,一直向下推,可以推出其中一组 d p i , 2 dp_{i,2} dpi,2解。
同理如果从第二行第一列从第二行第二列走。并且设其 d p 2 , 2 = d p 2 , 1 + a 2 , 2 dp_{2,2} = dp_{2,1} + a_{2,2} dp2,2=dp2,1+a2,2。这样一来也就达到了破环的效果,再用第一列的方法,一直向下推,可以推出又一组 d p i , 2 dp_{i,2} dpi,2解。
…
同理如果从第 n n n行第一列从第 n n n行第二列走。并且设其 d p n , 2 = d p n , 1 + a n , 2 dp_{n,2} = dp_{n,1} + a_{n,2} dpn,2=dpn,1+an,2。这样一来也就达到了破环的效果,再用第一列的方法,一直向下推,可以推出又一组 d p i , 2 dp_{i,2} dpi,2解。
然后在 d p i , 2 dp_{i,2} dpi,2 解组( n n n组解) 中选择最大的值当作 d p i , 2 dp_{i,2} dpi,2的解即可
然后第二列的答案都是定值了
以此类推直到最后一列
由此我们得到了第二个递推式:
设 f d p k , i , j fdp_{k,i,j} fdpk,i,j表示从第 k k k行第 j − 1 j-1 j−1列往第 k k k行第 j j j列走后推出的一组解,并且设其 f d p k , k , j = d p k , j − 1 + a k , j fdp_{k,k,j} = dp_{k,j-1} + a_{k,j} fdpk,k,j=dpk,j−1+ak,j
并且推出一组解的递推式为:
f d p k , i , j = f d p k , i − 1 , j + a i , j , ( i ≠ k ) fdp_{k,i,j} = fdp_{k,i-1,j} + a_{i,j} , (i \neq k) fdpk,i,j=fdpk,i−1,j+ai,j,(i=k)
得到所以 m m m组解后,真正的答案选取最大的即可:
d p i , j = max k = 1 n ( f d p k , i , j ) dp_{i,j} = \displaystyle\max_{k=1}^n{(fdp_{k,i,j})} dpi,j=k=1maxn(fdpk,i,j)
由第一列为定值推出第二列的值,可以将第二列看为定值推第三列,以此类推。
换句话说由第一列为定值推出第二列的值其实是枚举第二列的“起点”
#define RIP(i,j,k) for(int (i) = (j) ; (i) < (k) ; ++ i)
inline int f_i(int x){return x + 1 == n + 1 ? 1 : x + 1;}//往下
inline int i_f(int x){return x - 1 == 0 ? n : x - 1; }//往上
int ans[N][N];
bool vis[N][N]
RIP(j,1,m+1){for(int i = s ; !vis[i][j] ; i = f_i(i)){//枚举起点/转移行int ansi[N];//fdpvis[i][j] = true;ansi[i] = a[i][j] + ans[i][j-1];for(int k = f_i(i); k != i ; k = f_i(k)){ansi[k] = a[k][j] + ansi[i_f(k)];//fdp[i] = fdp[i-1] + a[i][j]}//得到一组答案RIP(k,1,n+1)ans[k][j] = max(ans[k][j] , ansi[k]);//更新这一组答案if(j==1)break;//第一列的起点就一个}}
注意第一列的起点就一个!
总时间复杂度为 O ( m × n 2 ) O(m\times n^2) O(m×n2),在简单版本可以通过,但困难版本数据范围放大后就会TLE
如果要考虑通过困难版本就需要优化
将递推式 d p i , j = max k = 1 n ( f d p k , i , j ) dp_{i,j} = \displaystyle\max_{k=1}^n{(fdp_{k,i,j})} dpi,j=k=1maxn(fdpk,i,j)展开就会有
d p i , j = max k = 1 n d p k , j − 1 + ∑ o = k i a o , j dp_{i,j} = \displaystyle\max_{k=1}^n dp_{k,j-1} + \displaystyle\sum_{o=k}^i a_{o,j} dpi,j=k=1maxndpk,j−1+o=k∑iao,j
其中 ∑ o = k i a o , j \displaystyle\sum_{o=k}^i a_{o,j} o=k∑iao,j表示从第 k k k行第 j j j列一直向下走直到第 i i i行第 j j j列的路径上的权值和。
那么重点是每次算这个路径权值和的时候是最里面的一层循环,并且还要求最大值。
for(int k = f_i(i); k != i ; k = f_i(k)){ansi[k] = a[k][j] + ansi[i_f(k)];//fdp[i] = fdp[i-1] + a[i][j]}//得到一组答案RIP(k,1,n+1)ans[k][j] = max(ans[k][j] , ansi[k]);//更新这一组答案
这时候如果路径权值和能立马得到那么循环常数就可能会减少。如果把这一列数字单独拿出来当作一个数组 b i b_i bi,其实就是要求 ∑ i = l r b i \displaystyle\sum_{i=l}^r b_i i=l∑rbi
相当于多次要求区间和,我们可以考虑前缀和预处理优化。
设 p r e n = ∑ i = 1 n b n = ∑ i = 1 n a n , j ( j 为固定值 ) pre_n = \displaystyle\sum_{i=1}^n b_n = \displaystyle\sum_{i=1}^n a_{n,j} (j为固定值) pren=i=1∑nbn=i=1∑nan,j(j为固定值)
因为是一列一列考虑的所以j为固定值。相当于原本是:
p r e n , j = ∑ i = 1 n a n , j pre_{n,j} =\displaystyle\sum_{i=1}^n a_{n,j} pren,j=i=1∑nan,j “滚动化”
所以以下式子默认 p r e n = p r e n , j pre_n = pre_{n,j} pren=pren,j
接下来改写递推式,继续展开
d p i , j = max d p k , j − 1 + { p r e i − p r e k − 1 , i ≥ k p r e n − p r e k − 1 + p r e i , i < k dp_{i,j} = \max dp_{k,j-1} + \begin{cases}pre_i - pre_{k-1} , i \ge k \\ pre_n - pre_{k-1} + pre_i , i \lt k \end{cases} dpi,j=maxdpk,j−1+{prei−prek−1,i≥kpren−prek−1+prei,i<k
将预处理后的共同已知量提到 max \max max外面得
d p i , j = p r e i + max d p k , j − 1 + { − p r e k − 1 , i ≥ k p r e n − p r e k − 1 , i < k dp_{i,j} = pre_i + \max dp_{k,j-1} + \begin{cases} - pre_{k-1} , i \ge k \\ pre_n - pre_{k-1} , i \lt k \end{cases} dpi,j=prei+maxdpk,j−1+{−prek−1,i≥kpren−prek−1,i<k
然而如果每个 i i i都枚举一遍 k k k时间复杂度依旧是 O ( m × n 2 ) O(m\times n^2) O(m×n2),这时候就需要预处理 k k k和 i i i的关系
假设 G i G_i Gi表示 max d p k , j − 1 − p r e k − 1 , i ≥ k \max dp_{k,j-1} - pre_{k-1}, i \ge k maxdpk,j−1−prek−1,i≥k
假设 L i L_i Li表示 max d p k , j − 1 + p r e n − p r e k − 1 , i < k \max dp_{k,j-1} + pre_n - pre_{k-1} , i \lt k maxdpk,j−1+pren−prek−1,i<k
那么原递推式就能写成
d p i , j = p r e i + max ( G i , L i ) dp_{i,j} = pre_i + \max (G_i,L_i) dpi,j=prei+max(Gi,Li)
递推式就是只用枚举 i i i
那么问题就在于如何用线性时间复杂度预处理解出 G i G_i Gi和 L i L_i Li
从式子上发现 G i G_i Gi只与 k k k有关。也就是说
G 1 = d p 1 , j − 1 − p r e 0 G_{1} = dp_{1,j-1} - pre_{0} G1=dp1,j−1−pre0 ,(相当于 k k k只能选 { 1 } \set{1} {1})
G 2 = max ( d p 1 , j − 1 − p r e 0 , d p 2 , j − 1 − p r e 1 ) = max ( G 1 , d p 2 , j − 1 − p r e 1 ) G_{2} = \max(dp_{1,j-1} - pre_{0} , dp_{2,j-1} - pre_{1}) = \max(G_1,dp_{2,j-1} - pre_{1}) G2=max(dp1,j−1−pre0,dp2,j−1−pre1)=max(G1,dp2,j−1−pre1) ,(相当于 k k k只能选 { 1 , 2 } \set{1,2} {1,2})
同理 G 3 = max ( G 2 , d p 3 , j − 1 − p r e 2 ) G_3 = \max{(G_2 , dp_{3,j-1} - pre_{2})} G3=max(G2,dp3,j−1−pre2)
以此类推 G i = max ( G i − 1 , d p i , j − 1 − p r e i − 1 ) G_i = \max{(G_{i-1} , dp_{i,j-1} - pre_{i-1})} Gi=max(Gi−1,dpi,j−1−prei−1)
L i L_i Li只与 k k k有关,
L n = − i n f ( 无 ) L_n = -inf (无) Ln=−inf(无) , (相当于 k k k没得选)
L n − 1 = d p n , j − 1 + p r e n − p r e n − 1 L_{n-1} = dp_{n,j-1} + pre_{n} - pre_{n-1} Ln−1=dpn,j−1+pren−pren−1,(相当于 k k k只能选 { n } \set{n} {n})
同理 L n − 2 = max ( L n − 1 , d p n − 1 , j − 1 + p r e n − p r e n − 2 ) L_{n-2} = \max{(L_{n-1},dp_{n-1,j-1} + pre_{n} - pre_{n-2})} Ln−2=max(Ln−1,dpn−1,j−1+pren−pren−2)
以此类推 L i = max ( L i + 1 , d p i + 1 , j − 1 + p r e n − p r e i ) L_i = \max{(L_{i+1} , dp_{i+1,j-1} + pre_n - pre_{i})} Li=max(Li+1,dpi+1,j−1+pren−prei)
RIP(j,1,m+1){int pre[N],L[N],G[N];RIP(i,0,n+1){pre[i] = 0;L[i] = G[i] = -inf;}RIP(i,1,n+1)pre[i] = pre[i-1] + a[i][j];RIP(i,1,n+1)G[i] = max(G[i-1],ans[i][j-1]-pre[i-1]);L[n] = -inf;L[n-1]=ans[n][j-1]+pre[n]-pre[n-1];rRIP(i,n-2,1)L[i]= max(L[i+1],ans[i+1][j-1] + pre[n] - pre[i]);RIP(i,1,n+1)ans[i][j] = pre[i] + max(L[i],G[i]);}
最后用上起点,和之前的思路一样。第一列只有一个起点,也就是说我们要保证
d p s , 0 = 0 且 d p s , 0 > > d p i , 0 ( i ≠ s ) dp_{s,0} = 0 且 dp_{s,0} >> dp_{i,0} (i\neq s) dps,0=0且dps,0>>dpi,0(i=s)(保证不能选到(有)从非起点行第0列转移到第一列的任意行的情况)
所以设置 d p i , 0 = − i n f ( i ≠ s ) dp_{i,0} = -inf (i\neq s) dpi,0=−inf(i=s)
RIP(i,1,n+1)ans[i][0]=-inf;ans[s][0]=0;
时间复杂度为 O ( m × n ) O(m\times n) O(m×n)
代码
#include <bits/stdc++.h>
#define int long long
#define RIP(i,j,k) for(int (i) = (j) ; (i) < (k) ; ++ i)
#define rRIP(i,j,k) for(int (i) = (j) ; (i) >= (k) ; -- i)
#define all(x) x.begin(),x.end()
#define endy {cout << "YES" << '\n'; return; }
#define endn {cout << "NO" << '\n'; return; }
#define endx(x) {cout << (x) << '\n'; return; }
#define conti(x) {cout << (x) << '\n';continue;}
#define IOS std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
using namespace std;
using i128 = __int128;
using pii = pair<int,int>;
using tiii = tuple<int,int,int>;
using vi = vector<int>;
using vvi = vector<vi>;
using vii = vector<pii>;
using mii = map<int,int>;
const int N = 2e3+10;
const int mod = 1e9 + 7;
const int MID = 2e4+10;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n = 1 , m = 100 , s , t;int a[N][N];
int ans[N][N];
int read() {int x = 0, w = 1;char ch = 0;while (ch < '0' || ch > '9') {if (ch == '-') w = -1;ch = getchar();}while (ch >= '0' && ch <= '9') {x = x * 10 + (ch - '0');ch = getchar();}return x * w;
}
inline int f_j(int x){return x + 1 == m + 1 ? 1 : x + 1; }
inline int j_f(int x){return x - 1 == 0 ? m : x - 1; }
inline int f_i(int x){return x + 1 == n + 1 ? 1 : x + 1;}
inline int i_f(int x){return x - 1 == 0 ? n : x - 1; }
void solve(){n = read() , m = read() , s = read() , t = read();RIP(i,1,n+1)RIP(j,1,m+1){a[i][j] = read();ans[i][j] = -inf;}RIP(i,1,n+1)ans[i][0]=-inf;ans[s][0]=0;RIP(j,1,m+1){int pre[N],L[N],G[N];RIP(i,0,n+1){pre[i] = 0;L[i] = G[i] = -inf;}RIP(i,1,n+1)pre[i] = pre[i-1] + a[i][j];RIP(i,1,n+1)G[i] = max(G[i-1],ans[i][j-1]-pre[i-1]);L[n] = -inf;L[n-1]=ans[n][j-1]+pre[n]-pre[n-1];rRIP(i,n-2,1)L[i]= max(L[i+1],ans[i+1][j-1] + pre[n] - pre[i]);RIP(i,1,n+1)ans[i][j] = pre[i] + max(L[i],G[i]);}cout << ans[t][m];
}
signed main(){//IOS//int T;cin >> T;while(T--)solve();
}
简单版本
#include <bits/stdc++.h>
#define int long long
#define RIP(i,j,k) for(int (i) = (j) ; (i) < (k) ; ++ i)
#define rRIP(i,j,k) for(int (i) = (j) ; (i) >= (k) ; -- i)
#define all(x) x.begin(),x.end()
#define endy {cout << "YES" << '\n'; return; }
#define endn {cout << "NO" << '\n'; return; }
#define endx(x) {cout << (x) << '\n'; return; }
#define conti(x) {cout << (x) << '\n';continue;}
#define IOS std::ios::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);
using namespace std;
using i128 = __int128;
using pii = pair<int,int>;
using tiii = tuple<int,int,int>;
using vi = vector<int>;
using vvi = vector<vi>;
using vii = vector<pii>;
using mii = map<int,int>;
const int N = 2e3+10;
const int mod = 1e9 + 7;
const int MID = 2e4+10;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n = 1 , m = 100 , s , t;int a[N][N];
int ans[N][N];
bool vis[N][N];
int pre[N][N];
int read() {int x = 0, w = 1;char ch = 0;while (ch < '0' || ch > '9') {if (ch == '-') w = -1;ch = getchar();}while (ch >= '0' && ch <= '9') {x = x * 10 + (ch - '0');ch = getchar();}return x * w;
}
inline int f_j(int x){return x + 1 == m + 1 ? 1 : x + 1; }
inline int j_f(int x){return x - 1 == 0 ? m : x - 1; }
inline int f_i(int x){return x + 1 == n + 1 ? 1 : x + 1;}
inline int i_f(int x){return x - 1 == 0 ? n : x - 1; }
void solve(){n = read() , m = read() , s = read() , t = read();RIP(i,1,n+1)RIP(j,1,m+1){a[i][j] = read();ans[i][j] = -inf;}RIP(j,1,m+1)RIP(i,1,n+1)pre[i][j] = pre[i-1][j] + a[i-1][j];RIP(j,1,m+1){for(int i = s ; !vis[i][j] ; i = f_i(i)){int ansi[N];vis[i][j] = true;ansi[i] = a[i][j] + ans[i][j-1];for(int k = f_i(i); k != i ; k = f_i(k)){ansi[k] = a[k][j] + ansi[i_f(k)];}RIP(k,1,n+1)ans[k][j] = max(ans[k][j] , ansi[k]);if(j==1)break;}}cout << ans[t][m];
}
signed main(){//IOS//int T;cin >> T;while(T--)solve();
}