您的位置:首页 > 其它

[codeforces704B]ant man 解题报告

2016-10-21 10:02 218 查看
题意

求1~n的排列中∑n−1i=1∣∣xpi−xpi+1∣∣+[pi<pi+1](dpi+api+1)+[pi>pi+1](cpi+bpi+1)的最大值,要求p1=s,pn=e,保证s≠e。

1≤ai,bi,ci,di,xi≤109,∀i∈[1,n],且xi≤xi+1,∀i∈[1,n)

n≤5000

从shallwe大爷那里学习了一个O(kn3)的费用流做法。

建两列点,源点向左边一列的n个点连容量为1费用为0的边,右边一列n个点也向汇点连容量为1费用为0的边,左边代表i的点向右边代表j的点连容量为1费用为|xi−xj|+[i<j](di+aj)+[i>j](ci+bj)(i≠j,i≠e,j≠s)的边。这样就可以保证每个点只经过一次,s的右侧会有至少一个点,e的左侧至少会有一个点,其他的点左右侧都会各有一个点,并且代价记在费用里,那么最小费用最大流就是答案。

注意到s在右侧、e在左侧的点是没有用的,所以我们可以把它们都删掉。这样的话就得到了一个n-1个点的完全图,求其最小权完美匹配!可以用KM算法,然而我并不会。。但是似乎也是O(n3)的。

不过受到启发,原来二分图的那套东西都是可以用网络流做的!那么对于普通的最大匹配,只需要把上图中的费用去掉即可。而且这样一来可以发现匈牙利算法中的增广路其实就是网络流中的增广路,已经匹配的边其实就是网络流中的反向边,那么这就可以轻易的解释匈牙利算法了!我原来学的时候还各种不明白,没想到竟然如此简单!!

注意到对于一个排列中的一个pi来说,它的贡献只与它两边的数与它的大小有关。

所以我们便可以有一个经典的O(n2)的dp做法。从小到大向排列中插入数。f(i,j)表示1~i在最终排列中被分成了j段,转移的话可以是新建一段、接在一段前/后、或连接两段。麻烦的地方在于s和e,插入s、e的时候和已经拥有s、e的段需要特殊考虑一下:s可以新建一段,如果s≠n,则s能接在e不在的段的左侧,如果s=n的话,则s可以接在任意一段的左侧;e可以新建一段,如果e≠n,则e能接在s不在的段的右侧,如果e=n,则e可以接在任意一段的右侧;对于不等于s也不等于e的i而言,可以新建一段,或者接在s不在的段的左侧,接在e不在的段的右侧,i也可以连接两段,但如果i≠n的话,只有有s、e都不在的段才能供其连接。

yveh和shallwe又教了我一个非常强的贪心做法。

一开始我们有{s,e},然后我们依次考虑1~n每一个除了s、e以外的数,将其加入到排列中,枚举插入到哪里会产生最小的增量,然后把它加在那里。

这样就可以得到最终的答案了!虽然时间复杂度也是O(n2)的,但是好写很多。

考虑一下这是为什么?

先不考虑s和e的影响,假设我们会把数插在中间。考虑一个数对答案的贡献,会发现这其实只跟它与左右两边数的大小关系有关。而且一个数x在刚加进去的时候它的形态是固定的,x两边的数必然都比它小(因为是从小往大插入的),而x可以让一个之前加进去的数y的某一侧由比y小变成比y大。所以就相当于是给出若干操作,每次从某数据结构中提出一个数加到答案里,向某数据结构中加入两个数(−2xi+bi−ai,−2xi+di−ci),使得最终结果最小。那么显然每次选最小值就可以了,只需要一个堆来维护。

注意到任何一种序列必然是可以通过这种操作序列来生成的,所以二者是等价的。

那么我们需要考虑s和e了。首先1其实是一个特例,所以我们的初始序列需要对1进行特判,如果1≠s,1≠e的话就是{s,1,e},否则就是{s,e}。我们首先算出初始序列的值,然后从i=2开始,如果i=s,就把−2xi+di−ci加入堆中;如果i=e,就把−2xi+bi−ai加入堆中;否则,先把2xi+ai+ci加进答案中,然后如果i < s,−2xi+bi−ai需要在查询之前加入堆中,如果i < e,−2xi+di−ci也需要在查询之前加入,然后把堆中最小值弹出并加入答案。

这样就得到了一个时间复杂度O(nlogn)而且常数非常小的做法。

不过我们当然也可以有一个常数较大的O(nα(n))的做法,这其实是一个并查集的经典问题。就是我们考虑每个插入可以被询问到的是一个关于时间的后缀,那么我们从大到小考虑每个插入的话,一个插入就应该被放到它能放到的最先的查询处,所以可以用一个并查集来维护一个查询时间点后面第一个还没被填的查询的位置。当然做到这些还需要O(n)的排序。

代码(dp):

#include<stdio.h>
#include<cstring>
#include<iostream>
using namespace std;
#include<algorithm>
const int N=5000+5;
typedef long long LL;
int main()
{
freopen("local23.in","r",stdin);
freopen("local23.out","w",stdout);

int n,s,e;
static int x
,a
,b
,c
,d
;
scanf("%d%d%d",&n,&s,&e);
for(int i=1;i<=n;++i)scanf("%d",x+i);
for(int i=1;i<=n;++i)scanf("%d",a+i);
for(int i=1;i<=n;++i)scanf("%d",b+i);
for(int i=1;i<=n;++i)scanf("%d",c+i);
for(int i=1;i<=n;++i)scanf("%d",d+i);

static LL f

;
memset(f,127,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;++i)
{
for(int j=min(i-1,n-i+2);j>=0;--j)
if(f[i-1][j]<=1e18)
if(i==s)
{
f[i][j+1]=min(f[i][j+1],f[i-1][j]+d[i]-x[i]);
if(i==n||j-(e<i)>0)f[i][j]=min(f[i][j],f[i-1][j]+c[i]+x[i]);
}
else if(i==e)
{
f[i][j+1]=min(f[i][j+1],f[i-1][j]+b[i]-x[i]);
if(i==n||j-(s<i)>0)f[i][j]=min(f[i][j],f[i-1][j]+a[i]+x[i]);
}
else
{
f[i][j+1]=min(f[i][j+1],f[i-1][j]+b[i]+d[i]-(x[i]<<1));
if(j-(s<i)>0)f[i][j]=min(f[i][j],f[i-1][j]+b[i]+c[i]);
if(j-(e<i)>0)f[i][j]=min(f[i][j],f[i-1][j]+a[i]+d[i]);
if(j>1)
if(i==n)f[i][j-1]=min(f[i][j-1],f[i-1][j]+a[i]+c[i]+(x[i]<<1));
else if(j-(s<i)-(e<i)>0)f[i][j-1]=min(f[i][j-1],f[i-1][j]+a[i]+c[i]+(x[i]<<1));
}

/*for(int j=min(i,n-i+1);j;--j)
if(f[i][j]<=1e18)
printf("f(%d,%d)=%I64d\n",i,j,f[i][j]);*/
}

cout<<f
[1]<<endl;
}


代码(贪心):

#include<stdio.h>
#include<iostream>
using namespace std;
#include<algorithm>
#include<cstring>
#include<queue>
#include<cstdlib>
#include<vector>
const int N=5000+5;
typedef long long LL;
int x
,a
,b
,c
,d
;
LL cal(int i,int j)
{
if(i<j)return -(LL)x[i]+x[j]+d[i]+a[j];
else return (LL)x[i]-x[j]+c[i]+b[j];
}
int main()
{
//freopen("local23.in","r",stdin);
//freopen("local23_greed.out","w",stdout);

int n,s,e;
scanf("%d%d%d",&n,&s,&e);
for(int i=1;i<=n;++i)scanf("%d",x+i);
for(int i=1;i<=n;++i)scanf("%d",a+i);
for(int i=1;i<=n;++i)scanf("%d",b+i);
for(int i=1;i<=n;++i)scanf("%d",c+i);
for(int i=1;i<=n;++i)scanf("%d",d+i);

LL ans=0;
if(min(s,e)==1)ans+=cal(s,e);
else ans+=cal(s,1)+cal(1,e);
priority_queue<LL,vector<LL>,greater<LL> > heap;
LL A,B,C,D;
for(int i=2;i<=n;++i)
{
A=x[i]+a[i],B=-x[i]+b[i],C=x[i]+c[i],D=-x[i]+d[i];
if(i==s)heap.push(D-C);
else if(i==e)heap.push(B-A);
else{
ans+=A+C;
if(i<s)heap.push(B-A);
if(i<e)heap.push(D-C);
ans+=heap.top();
heap.pop();
if(i>s)heap.push(B-A);
if(i>e)heap.push(D-C);
}
}
cout<<ans<<endl;
}


总结:

①匈牙利算法原来其实就是网络流!

②遇到什么题的时候一定要大胆想一下能不能网络流?感觉对网络流非常不敏感啊!

③一个排列可以通过从1->n插入来生成,可以根据这种思路来对其贪心。

而另一个思路是考虑交换相邻两个元素的变化贡献,这样做的正确性在于如果可以搞出一个比较器,那么有逆序对的便都不是最优值,而排过序后的序列两两之间都能以0的代价互达,所以是唯一的(一片)极值,便可以说明其是最优值了。(在有限域中,唯一极小值等价于最小值)

④一个经典的贪心:支持插入一个数,提取一个值。使得提取的值的和最小。可以用堆/并查集。

⑤一个很重要的想法,考虑时间维:顺序/倒序,或者是将其与原操作序列互反。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: