ABC334 F&G
F:
我们可以更好的利用一操作——当且仅当钱不够用时,加上经过的所有点中最大的 \(P\),也就是在那个点插入一次一操作。
设 \(dp_{i,j,x,y}=(step,money)\) 表示到达点 \((i,j)\),经过的最大的 \(P\) 在点 \((x,y)\),最少需要 \(val\) 次一操作(移动操作最后算上就行了),此前提下手头的钱最多为 \(-money\)(方便比较)。
转移时,先枚举 \((i,j)\),再枚举 \((x,y)\),向 \((i,j+1),(i+1,j)\) 刷表即可。
正确性显然。因为我们是在钱不够用时才操作,所以对于任意 \(dp_{i,j,x,y}\),即使其他情况下手头的钱更多,也一定小于 \(P_{x,y}\)。我们只要一次操作,钱就可以超过它们,且操作数不劣。
具体 \(dp\) 过程见代码。
#include<algorithm>
#include<cstdio>
#include<cstring>
#define pii pair<int,int>
#define mk make_pair
#define ft first
#define se second
#define pb push_back
#define db double
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define inf 1e18
using namespace std;
namespace IO{
#ifndef LOCAL
#define SIZE 30000
char in[SIZE],out[SIZE],*p1=in,*p2=in,*p3=out;
#define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,SIZE,stdin),p1==p2)?EOF:*p1++)
#define flush() (fwrite(p3=out,1,SIZE,stdout))
#define putchar(ch) (p3==out+SIZE&&flush(),*p3++=(ch))
class Flush{public:~Flush(){fwrite(out,1,p3-out,stdout);}}_;
#endif
template<typename type>
inline void read(type &x){
x=0;bool flag=0;char ch=getchar();
while(ch<'0'||ch>'9') flag^=ch=='-',ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
flag?x=-x:0;
}
template<typename type>
inline void write(type x,char ch=0){
x<0?x=-x,putchar('-'):0;static short Stack[50],top=0;
do Stack[++top]=x%10,x/=10;while(x);
while(top) putchar(Stack[top--]|48);
if(ch) putchar(ch);
}
}
using namespace IO;
#define M 85
int n,p[M][M],r[M][M],d[M][M];
pair<ll,ll> dp[M][M][M][M];
int main(){
read(n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
read(p[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<n;j++){
read(r[i][j]);
}
}
for(int i=1;i<n;i++){
for(int j=1;j<=n;j++){
read(d[i][j]);
}
}
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){
for(int x=1;x<=n;x++) for(int y=1;y<=n;y++){
dp[i][j][x][y]=mk(inf,inf);
}
}
dp[1][1][1][1]=mk(0,0);
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){
for(int x=1;x<=i;x++) for(int y=1;y<=j;y++){
if(dp[i][j][x][y].ft>1e14) continue;
if(i+1<=n){
ll need=dp[i][j][x][y].se+d[i][j],opr=0;
if(need>0){
opr=(need+p[x][y]-1)/p[x][y];
need-=opr*p[x][y];
}
int nx=x,ny=y;
if(p[i+1][j]>p[nx][ny]) nx=i+1,ny=j;
dp[i+1][j][nx][ny]=min(dp[i+1][j][nx][ny],mk(dp[i][j][x][y].ft+opr,need));
}
if(j+1<=n){
ll need=dp[i][j][x][y].se+r[i][j],opr=0;
if(need>0){
opr=(need+p[x][y]-1)/p[x][y];
need-=opr*p[x][y];
}
int nx=x,ny=y;
if(p[i][j+1]>p[nx][ny]) nx=i,ny=j+1;
dp[i][j+1][nx][ny]=min(dp[i][j+1][nx][ny],mk(dp[i][j][x][y].ft+opr,need));
}
}
}
ll ans=inf;
for(int x=1;x<=n;x++){
for(int y=1;y<=n;y++){
ans=min(ans,dp[n][n][x][y].ft);
}
}
write(ans+2*(n-1),'\n');
return 0;
}
G:
官方题解给出了一种巧妙的做法。下面再提一提。
由 \(A*X+B<=Y \rightarrow -A*X+Y>=B\),想到若能使所有点按 \(-A*X+Y\) 的值从小到大排序,则可以用二分解决询问。
不妨将询问按 \(A\) 从小到大排序。
初始情况可以视作 \(A=-\infty\),此时将 \((X,Y)\) 按字典序从小到大排序即可。
将 \((X,Y)\) 考虑成 \(f(A)=(-X)*A+Y\),不难发现随着 \(A\) 的增大,任意相邻两点间最多交换一次顺序,交换点即两直线交点的横坐标.
具体地,对于 \((X_1,Y_1),(X_2,Y_2)\),交换点即为 \(\frac{Y_2-Y_1}{X_2-X_1}\)。
接下来,只要考虑每次回答询问前完成为数不多(\(O(n^2)\))的交换即可。
下面是具体实现:
开始时,把所有交换点放进优先队列,堆顶为最小的交换点。
每次询问前,若堆顶值小于当前的 \(A\),则进行交换,并将左右两边新的可能加进去,将堆顶弹出。
特别注意,堆顶值对应的两条直线此时可能已经不相邻,若如此应忽略这个值并将其弹出。
一个简单二分回答询问即可。

浙公网安备 33010602011771号