【题解】P11303 [NOISG 2021 Finals] Pond
P11303 [NOISG 2021 Finals] Pond
题意
有 \(n\) 个路灯,路灯 \(i\) 与路灯 \(i+1\) 距离为 \(d_i\),每秒没有被关闭的路灯都会消耗 \(1\) 电能。
你初始时在 \(m\),每秒可以移动 \(1\) 单位距离,在移动过程中关闭所经过的路灯,直到所有路灯被关闭。
最小化消耗的总电能。
\(n\le 3\times 10^5\)。
题解
知识点:动态规划,斜率优化,李超线段树。
启发:
-
贡献提前计算。
-
代价分开计算。
不妨对 \(d_i\) 做前缀和,得到 \(a_i\),表示 \(i\) 的位置,\(a_1=0\)。
首先有一个不需要怎么动脑子的区间 dp,设 \(dp_{l,r,0/1}\) 为关闭了 \(l\sim r\) 的路灯,最后停在左/右端点的最小消耗。
初始条件:\(dp_{m,m,0}=dp_{m,m,1}=0\)。
答案:\(\min (dp_{1,n,0},dp_{1,n,1})\)。
有如下转移:
上面用到了贡献提前计算的思想,因为对于每个路灯,很难刻画他什么时候被关闭,所以使用一种巧妙的方法来变相维护。
以第一个转移第一个式子为例,从 \(l+1\) 走到 \(l\),耗时 \(a_{l+1}-a_l\),这个过程中,\(1\sim l\) 与 \(r+1\sim n\) 的路灯都在消耗电能,所以加上 \((l+n-r)(a_{l+1}-a_l)\) 的消耗。
时间复杂度 \(O(n^2)\)。
代码如下,结合 \(d_i=1\) 的特判,可以获得 \(24\) 分。
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define per(i,l,r) for(int i=(r);i>=(l);--i)
#define pr pair<int,int>
#define fi first
#define se second
#define pb push_back
#define all(x) (x).begin(),(x).end()
#define sz(x) (int)(x).size()
#define bg(x) (x).begin()
#define ed(x) (x).end()
#define N 300010
#define int long long
int n,m,a[N],dp[2025][2025][2];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
bool one=1;
rep(i,2,n){
int x;
cin>>x;
a[i]=a[i-1]+x;
one&=(x==1);
}
if(one){
int ans1=0,ans2=0;
rep(i,1,m){
ans1+=(a[m]-a[i]);
}
rep(i,m+1,n){
ans1+=(a[m]-a[1])*2+a[i]-a[m];
}
rep(i,m,n){
ans2+=(a[i]-a[m]);
}
rep(i,1,m-1){
ans2+=(a[n]-a[m])*2+a[m]-a[i];
}
cout<<min(ans1,ans2);
return 0;
}
rep(i,1,n){
dp[i][i][0]=dp[i][i][1]=3e18;
}
dp[m][m][0]=dp[m][m][1]=0;
rep(len,2,n){
rep(l,1,n){
int r=l+len-1;
if(r>n){
break;
}
dp[l][r][0]=min({dp[l+1][r][0]+(a[l+1]-a[l])*(l+n-r),dp[l+1][r][1]+(a[r]-a[l])*(l+n-r)});
dp[l][r][1]=min({dp[l][r-1][0]+(a[r]-a[l])*(l+n-r),dp[l][r-1][1]+(a[r]-a[r-1])*(l+n-r)});
}
}
cout<<min(dp[1][n][0],dp[1][n][1]);
return 0;
}
但是 \(n\le 3\times 10^5\),区间 dp 的状态包含两个端点,状态数十分爆炸,能否拆开两个端点,分别考虑?
通过上面的观察,可以发现答案由两部分组成,每个路灯到第 \(m\) 个路灯的距离之和加上额外的电能消耗,前者是固定的,所以考虑最小化后者。
先思考最优的移动路径长什么样子,肯定是从 \(m\) 出发,走到 \(m\) 的一边,再掉头走到 \(m\) 的另一边,不会出现类似在 \(m\) 的右边,往左走但没走到 \(m\),又转头往右走的情况,这种是纯浪费。
可以发现,每一次的掉头走一段距离,都会关闭新的路灯,也都会跨过 \(m\)。
那就找到了分界点 \(m\),接着将两个端点分开,定义 \(f_i(i\le m)\) 表示走到 \(i\) 位置的最小消耗,\(g_i(i\ge m)\) 表示走到 \(i\) 位置的最小消耗。
根据上面的推理,\(f_i\) 一定是从 \(g_i\) 转移过来(跨过 \(m\)),反之亦然,可以得出以下转移:
\(\displaystyle f_i=\min_{j\ge m} g_j+2(a_j-a_i)(n-j)\)
\(\displaystyle g_i=\min_{j\le m} f_u+2(a_i-a_j)(j-1)\)
依旧是运用了贡献提前计算的思想,以第一个式子为例,\(f_i\) 从 \(g_j\) 转移过来,相当于从 \(j\) 走到 \(i\),虽然距离为 \(a_j-a_i\),但是还是会走回去的(或者从 \(g_n\) 转移过来,但是此时没有额外贡献),且一定会再次经过 \(j\),此时路程为 \(2(a_j-a_i)\),相比于正常走,第 \(j+1\sim n\) 个路灯晚到了 \(2(a_j-a_i)\),所以提前计算他们的贡献,显然 \(1\sim i-1\) 的贡献不用计算。
发现无法确定转移的顺序,此时需要的是一个类 dijkstra 状物式的拓展,设定 \(l=r=m\),表示当前 \(f_l\) 和 \(g_r\),每次 \(f_l\) 选择当前最优的 \(g_i(i\in[m,r])\) 往左拓展,同样地每次 \(g_r\) 选择当前最优的 \(f_i(i\in[l,m])\) 往右拓展,当两者都能拓展时,贪心地选择 dp 值最小的拓展,这基于的是 \(g_i,f_i\) 都是单调的,根据状态的定义就能轻易证明。
最终可以得到一个 \(O(nk)\) 的做法。
再转过头看转移式,发现可以斜率优化,所以直接上李超线段树,复杂度 \(O(n \log n)\)。
#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define per(i,l,r) for(int i=(r);i>=(l);--i)
#define pr pair<int,int>
#define fi first
#define se second
#define pb push_back
#define all(x) (x).begin(),(x).end()
#define bg(x) (x).begin()
#define ed(x) (x).end()
#define sz(x) (int)(x).size()
#define N 300010
#define int long long
int n,m,a[N],f[N],g[N];
struct lct{//lichao
#define mid ((l+r)>>1)
pr tr[N<<2];
inline int calc(pr u,int x){
return u.fi*x+u.se;
}
inline void build(int k,int l,int r){
tr[k]={1e7,1e18};
if(l==r){
return;
}
build(k*2,l,mid);
build(k*2+1,mid+1,r);
}
inline void ref(int k,int l,int r,pr u){
if(calc(u,a[mid])<calc(tr[k],a[mid])){
swap(u,tr[k]);
}
if(l==r){
return;
}
if(calc(u,a[l])<calc(tr[k],a[l])){
ref(k*2,l,mid,u);
}
if(calc(u,a[r])<calc(tr[k],a[r])){
ref(k*2+1,mid+1,r,u);
}
}
inline int ask(int k,int l,int r,int x){
if(l==r){
return calc(tr[k],x);
}
int ans=calc(tr[k],x);
if(x<=a[mid]){
ans=min(ans,ask(k*2,l,mid,x));
}
else{
ans=min(ans,ask(k*2+1,mid+1,r,x));
}
return ans;
}
#undef mid
}ft,gt;
inline void insf(int i){
ft.ref(1,1,n,{2*(i-1),-2*(i-1)*a[i]+f[i]});
}
inline void insg(int i){
gt.ref(1,1,n,{2*(i-n),2*(n-i)*a[i]+g[i]});
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
rep(i,2,n){
cin>>a[i];
a[i]+=a[i-1];
}
ft.build(1,1,n);
gt.build(1,1,n);
int l=m,r=m;
insf(l);
insg(r);
while(l>1||r<n){
if(l==1){
r++;
g[r]=ft.ask(1,1,n,a[r]);
insg(r);
continue;
}
if(r==n){
l--;
f[l]=gt.ask(1,1,n,a[l]);
insf(l);
continue;
}
int resl=gt.ask(1,1,n,a[l-1]),resr=ft.ask(1,1,n,a[r+1]);
if(resl<=resr){
l--;
f[l]=resl;
insf(l);
}
else{
r++;
g[r]=resr;
insg(r);
}
}
int ans=min(f[1],g[n]);
rep(i,1,n){
ans+=abs(a[i]-a[m]);
}
cout<<ans<<"\n";
return 0;
}