题解:NWERC 2020 Bulldozer
没有看懂简洁写法/ll 于是学了官方题解的做法。语言拙劣,随意记录。
首先有一种基本的决策,就是把一个方块就地埋葬。用一次操作把两列拉开形成一个空位,再用一次操作把这个空位填上。
埋葬一个方块的代价是 \(2\)。
其实在一些情况下会有更优的方案,比如最右边一列,直接扔到右边的空地上明显比就地埋葬要好,因为这样解决单个方块只需要 \(1\) 的代价。我们称这种操作为推平吧。
所以我们最终的策略是,一些块往左推平,一些块往右推平,一些块就地埋葬。最左边和最右边是无穷远,所以过去的一定是一整堆一整堆的(因为推平操作单次更优)。
然后完全想不到的是,官方题解用最短路解决了这题。感觉这题建图有一点网络流的意思(?)
先建立超级源汇 \(S,T\) 表示左边和右边的无穷远。本来是要給每个方块一个编号的,但这样就爆炸了,所以先给没堆底部和顶部各一个编号。本文中每堆的流动方向是从底部到顶部。
下面开始建有向图,有点绕的。
- \(S\) 向每个堆顶连一条边,边权是前缀和 \(pre_i\),表示最左边的连续堆的推平操作,直接把左边的堆解决掉了(所以是连堆顶)。
- 同理,每个堆的底部和 \(T\) 连一条权值为后缀和 \(suf_i\) 的边。
- \(i\) 的堆顶连接 \(i+1\) 的底部,把相邻两堆连接起来。
- 每个堆的堆底向堆顶连一条权值为 \(2 \times a_i\) 的边,意思是我们用 \(2\) 的单位代价解决掉一整堆。
- 单个块推平的建边。
接下来就是最麻烦的单个方块推平到空位的建图。
假设现在在考虑向右推平:
- 如果当前位置为空,如果栈中有未匹配的方块,就匹配。
- 如果当前位置块数大于 \(1\),那么入栈。
所以就可以对每一堆求出如果向右推平,会推到哪些空位,给每堆开一个 \(vector\) 存位置就可以了,因为空位数最多是 \(O(n)\) 的,所以空间复杂度是对的。
同理,我们可以求出如果向左推平,每堆会匹配哪些位置。
我们可能会用到一些单个方块,可是之前建点都是整堆的。给每个块开编号是不现实的,而有用的(匹配上的)点只有上文提到的 \(O(n)\) 个,所以相当于是要在堆中建一些切割点,把堆分裂开。
分裂开之后再和对应的匹配位置连边(边权为坐标差)就可以了。另外,因为我们分裂了一些点,而最开始的单个埋葬的决策是从堆底连到堆顶的,所以我们需要对每堆相邻的分裂点连边,边权为它们之间夹着的方块数再乘 \(2\)。
离散化的细节还是比较多的。
最后只需要对 \(S\) 到 \(T\) 跑最短路就可以了。
#include<bits/stdc++.h>
#define fi first
#define se second
#define For(i,il,ir) for(int i=(il);i<=(ir);++i)
#define Rof(i,ir,il) for(int i=(ir);i>=(il);--i)
using namespace std;
typedef long long ll;
typedef pair<ll,int> pli;
typedef pair<int,int> pii;
const int N=1e6+10;
int n;
ll a[N],pre[N],suf[N];
int head[N<<2],cnt=1;
struct edge{
ll w;
int v,nxt;
}e[N<<2];
void add(int x,int y,ll z){ e[cnt].v=y,e[cnt].w=z,e[cnt].nxt=head[x],head[x]=cnt++; }
int idtot,S,T;
ll dis[N<<1];
bool vis[N<<1];
priority_queue<pli,vector<pli>,greater<pli> > q;
ll Dijkstra(){
memset(dis,0x3f,sizeof(dis));
dis[S]=0,q.push(make_pair(0,S));
while(!q.empty()){
int u=q.top().se; q.pop();
if(vis[u]) continue;
vis[u]=true;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w)
dis[v]=dis[u]+e[i].w,q.push(make_pair(dis[v],v));
}
}
return dis[T];
}
stack<int> st;
vector<int> lft[N],rgt[N];
int num[N],h[N<<1],g[N<<1],ht;
int gettop(){ int j=st.top();if(!--num[j]) st.pop(); return j; }
signed main()
{
scanf("%d",&n);
For(i,1,n) scanf("%lld",&a[i]);
S=n+n+1,idtot=T=n+n+2;
add(S,1,0);
For(i,1,n) add(i,i+n,max(a[i]-1,0ll)*2);// 埋葬第 i 堆
For(i,1,n-1) add(i+n,i+1,0);// i 的 top 和 i+1 的 bottom
add(n+n,T,0);
For(i,1,n) pre[i]=pre[i-1]+a[i],add(S,i+n,max(pre[i]-1,0ll));//推平一个前缀
Rof(i,n,1) suf[i]=suf[i+1]+a[i],add(i,T,max(suf[i]-1,0ll));//推平一个后缀
For(i,1,n)
if(a[i]>1) num[i]=a[i]-1,st.push(i);
else if(!a[i] && !st.empty()) rgt[gettop()].push_back(i);
while(!st.empty()) st.pop();
Rof(i,n,1)
if(a[i]>1) num[i]=a[i]-1,st.push(i);
else if(!a[i] && !st.empty()) lft[gettop()].push_back(i);
For(i,1,n) if(a[i]>1)//补到空列(离散化)
{
ht=0,h[0]=0,g[0]=i;
reverse(rgt[i].begin(),rgt[i].end());
int tot=0;
for(int p:lft[i]) h[++ht]=++tot,g[ht]=++idtot,add(p,g[tot],i-p);
tot=a[i]-rgt[i].size()-2;
for(int p:rgt[i])
if(++tot<=ht) add(g[tot],p+n,p-i);
else h[++ht]=tot,g[ht]=++idtot,add(g[ht],p+n,p-i);
h[++ht]=a[i]-1,g[ht]=i+n;
For(j,0,ht-1)
add(g[j],g[j+1],2ll*(h[j+1]-h[j]));
}
printf("%lld\n",Dijkstra());
return 0;
}

浙公网安备 33010602011771号