【Codeforces 321E / BZOJ 5311】【DP凸优化】【单调队列】贞鱼


目录
  • 题意:
  • 输入格式
  • 输出格式
  • 思路:
    • DP凸优化的部分
    • 单调队列转移的部分
    • 坑点
  • 代码

题意:

有n条超级大佬贞鱼站成一行,现在你需要使用恰好k辆车把它们全都运走。要求每辆车上的贞鱼在序列中都是连续的。每辆车上的贞鱼会产生互相怨恨的值,设a与b之间的怨恨值为G(a,b),一辆车上的贞鱼的编号从L到R,那么这辆车上的怨恨值为\(\sum_{L<=a,b<=R}G(a,b)\)。注意G(a,b)=G(b,a),一对贞鱼之间的怨恨值只算一次,也就是G(a,b)和G(b,a)只算一次。
1<=n<=4000,1<=k<=min(n,800),0<=G(a,b)<=10

输入格式

第一行两个整数n,k;
接下来是一个n*n的矩阵表示G[][],其中G(i,i)=0.且保证矩阵是对称的。

输出格式

一个整数ans表示怨恨值值之和的最小值。

思路:

DP凸优化的部分

首先,恰好使用k辆车这个限制非常的迷,不禁使我们想到了一个算法:DP凸优化。(要是平时拿给我做,我绝对想不到这玩意)至于什么是DP凸优化,这里就先行跳过讲解了,可以在里学习一下。

至于这道题,我们发现可以直接二分增加一辆车需要额外增加的代价,就可以限制车的数量。(代价越大,多使用一辆车就可能比较亏,还不如在一辆车里多塞几条贞鱼)。

然后就可以在二分出来的额外代价的意义下跑一个\(O(n^2)\)的dp了,总时间为\(O(n^2\log n)\),要被卡。

单调队列转移的部分

下面考虑如何优化。

首先还是看一下n*n的转移式是怎样的:

\[dp[i]=min(dp[j]+\frac{sum[i][i]-sum[i][j]-sum[j][i]+sum[j][j]}{2}) \]

其中dp[i]表示当前以第i条贞鱼为最后一辆车的最后一条鱼的最小值。然后sum[i][j]为给出的矩阵的前缀和。你会发现后面那一坨就是矩阵中(j+1,j+1)到(i,i)的和,也就是从j+1到i这些贞鱼互相怨恨的值的总和。但是由于题目的要求,还要除以2。

优化的话,仔细观察一波之后可以发现这玩意的转移时具有单调性的(前提是你dp的定义一定要是对的)。
选择单调性的话,是和dp值的增长率的大小有关的。如果不懂的话,可以看一下下面这个图。
首先将式子中的对于i而言的非常数项提出来:-(sum[i][j]+sum[j][i]-sum[j][j]),然后下面的图表现的是括号里面(先令作F(i,j))的几何意义。

j1

因为我们要求的是最小值,而此时如果dp[j1]还大于dp[j2]的话,在i增大的过程中,j1是绝对不可能成为决策点的。于是就可以把j1舍去了。

这里博主可能说的有些复杂,能理解就好。

这样子就可以利用决策单调性把整体时间复杂度优化到\(O(n\log n\log maxval)\)了。
至于决策单调性的具体过程,大概就是先发现一个决策点对一个连续的区间进行转移,所以可以在队列里面放入一个决策点当前能够更新的左右端点和自己的下标。

首先先默认能够更新到n,然后在之后的插入决策点的过程中,将绝对不可能进行之后的转移的点弹掉,然后在一个完整的区间内部进行二分,将这个区间拆成两个,其中右边的那个就是新插进去的决策点的更新区间啦。(具体参见代码)

坑点

1.转移式一定要记得/2
2.DP凸优化的时候,注意外层二分的写法,最好是自己调一下。
3.在最后算答案加回多算的代价的时候,记得是每辆车多算的代价乘上k,而不是乘上你这个方案的车的数量
4.在DP的转移过程中,除了要将值的大小设为第一比较关键字外,还要将选取车的数量设为第二关键字进行比较。通常是这样的:如果你在外层的二分中是判定当前选取车的数量<= k的时候算答案的话,你在内层就要保证在dp值相同的情况下,尽可能地少使用车;反之亦然(全部反过来就行了)。

代码

#include
#include
#include
#include
#include
#define MAXN 4000
#define MAXK 800
#define INF 0x7FFFFFFF
using namespace std;
struct node
{
	int l,r,j;
	node(){};
	node(int _l,int _r,int _j):l(_l),r(_r),j(_j){};
};
int n,dp[MAXN+5],dpnum[MAXN+5],sum[MAXN+5][MAXN+5];
int k;
node que[MAXN+5];
int Get(int j,int i)
{
	return dp[j]+(sum[i][i]-sum[i][j]-sum[j][i]+sum[j][j])/2;//记得除以2
}
int Cmp(int v1,int v2,int num1,int num2)//专门用来进行双关键字比较的函数
{
	if(v1v2)
		return -1;
	else if(num1>num2)
		return -1;
	else if(num1'9'){if(ch=='-')f=-1;ch=gc;}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=gc;}
    return x*f;
}
int main()
{
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		{
			sum[i][j]=read();
//			scanf("%d",&sum[i][j]);
			sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
		}
	int L=0,R=INF,mid;
	while(L>1LL;
		int ret=Check(mid);
		if(ret<=k)
			R=mid;//前者
		else
			L=mid+1;
	}
	int ret=Check(R);
	int val=dp[n]-R*k;//注意是-R*k而不是-R*ret,因为注重的是答案,而不是方案
	printf("%d\n",val);
	return 0;
}