说在前面
肝这题肝了几天
边比赛边肝题,终于肝出来了…开心qwq
题目
UPD:此题题源为codeforces321E
BZOJ5311传送门
题目大意
给定一个长度为
n
n
的序列,现在你需要将这个序列划分成连续的 段
被划分到同一段的两个位置
i,j
i
,
j
,会产生代价
aij
a
i
j
,不同段不会产生代价
现在请求出最小的贡献
范围:
n≤4000
n
≤
4000
,
k≤800
k
≤
800
,代价矩阵
aij
a
i
j
的数字均在
[0,10]
[
0
,
10
]
之间
保证:
aij=aji,aii=0
a
i
j
=
a
j
i
,
a
i
i
=
0
要求:一对
(i,j)
(
i
,
j
)
的代价只计算一次
输出格式
输入格式:
第一行两个整数
n,k
n
,
k
,含义如题
接下来是一个
n∗n
n
∗
n
的矩阵,第
i
i
行第 列的矩阵代表
aij
a
i
j
输出格式:
输出一行一个整数,表示答案
解法
这个题,me在BZOJ上T了一版,后来换了个方法终于过了
(不知道是不是me想复杂了,但me觉得这是一个很好的题
算法0
首先,我们可以写出一个很显然的DP
定义
dpi,j
d
p
i
,
j
表示,前
j
j
个分 组的最小代价
转移
dpi,j=min{dpi−1,k+cost[j+1][k]}
d
p
i
,
j
=
min
{
d
p
i
−
1
,
k
+
c
o
s
t
[
j
+
1
]
[
k
]
}
如果我们把代价矩阵
i>j
i
>
j
的部分变成
0
0
并求前缀和,转移就可以写成这样
复杂度
n2k
n
2
k
算法1
考虑对算法1进行优化
我们发现,随着
j
j
的变大,靠前的转移点会慢慢变得不优。具体的,转移具有单调性如图,如果从 转移,代价为
s1+s2
s
1
+
s
2
,如果从
k′
k
′
转移,代价为
s2
s
2
。因为代价都是正的,所以从
k
k
转移的代价不会小于从 转移的代价,并且差值
s1
s
1
可能越来越大
然而,由于
dpi−1,k<dpi−1,k′
d
p
i
−
1
,
k
<
d
p
i
−
1
,
k
′
,因此并没有显然的单调性
但我们知道,一旦在某刻时刻,
k′
k
′
比
k
k
优,那么它将永远比 优
所以,我们可以用一个单调队列来维护这些转移点。保证单调队列里 后一个点超越前一个点的时间单调即可。这也是 [JSOI2011]柠檬 的一种可行做法
时间复杂度
nk log2n
n
k
log
2
n
算法2
我们发现时限太小了!只有3s,而且读入数据高达 4∗106 4 ∗ 10 6 个整数,算法2仍然是不可过的,但是由于me没有想出进一步的优化,于是考虑换思路
重新审视这道题,
n
n
的数列,恰好 段,而且随着
k
k
的减小,答案会越来越大,并且,这个
δk
δ
k
也越来越大
这启发我们想到dp凸优化(又称带权二分 / WQS 二分)
这是一类二分方法,专门用于限制「恰好
k
k
个」且「单调」的题目中
复杂度 n log2n log2∑aij n log 2 n log 2 ∑ a i j ,加上fread可通过此题
算法3
这是CF上原题的解法传送门
还是记
dpi,j
d
p
i
,
j
表示前
j
j
个分 组的最小代价
然后把
dpi,j
d
p
i
,
j
最优转移的最小位置记作
opti,j
o
p
t
i
,
j
,即若
dpi,j=dpi−1,k+cost[k+1][j]
d
p
i
,
j
=
d
p
i
−
1
,
k
+
c
o
s
t
[
k
+
1
]
[
j
]
,则
opti,j=k
o
p
t
i
,
j
=
k
那么在
i
i
相同的时候,显然有
根据这一性质,我们考虑分治,我们花费
O(n)
O
(
n
)
先求出
dpi,mid
d
p
i
,
m
i
d
和
opti,mid
o
p
t
i
,
m
i
d
,这样,我们就可以知道
dpi,1...mid−1
d
p
i
,
1...
m
i
d
−
1
的决策点都不会超过
opti,mid
o
p
t
i
,
m
i
d
(
mid+1⋯n
m
i
d
+
1
⋯
n
同理),然后继续分治下去即可
伪代码大概长这样:
void solve( int d , int L , int R , int optL , int optR ){
if( L > R ) return ;
if( L == R ) special_work ;
int mid = ( L + R ) >> 1 ;
get_DP( dp[d][mid] , optL , optR ) ;//here we know opt[d][mid] as well
solve( d , L , M - 1 , optL , opt[d][mid] ) ;
solve( d , M + 1 , R , opt[d][mid] , optR ) ;
}
关于复杂度,因为同一层的决策区间加起来总长为 n n ,而相邻决策区间最多重合 (就是重合了 optd,mid o p t d , m i d ),所以一层的复杂度上限 2n 2 n ,一共log层,所以单次复杂度 nlogn n log n ,总复杂度 nklog2n n k log 2 n
卧槽为什么我写完了才发现这个方法的复杂度也那么大??还是过不去BZOJ上那个神tm时限
算法4
这个方法很有意思,me还没有仔细研究,不过感觉推广性挺强的
这个题的最优决策,其实可以看作是把这个序列分的尽量「平均」。「平均」是指,每一段的代价尽量平均
我们还是定义 opti,j o p t i , j 为 dpi,j d p i , j 的最优转移点,那么可以发现一个性质: opti−1,j≤opti,j≤opti,j+1 o p t i − 1 , j ≤ o p t i , j ≤ o p t i , j + 1
我们来感性理解一下这个性质: dpi−1,j d p i − 1 , j 可以看作是,在前 j j 个里画 根线使其尽量「平均」, opti−1,j o p t i − 1 , j 相当于是最后一根线的位置。同理, dpi,j d p i , j 是在前 j j 个里画 根线, opti,j o p t i , j 是最后一根线的位置。因为要「平均」,所以 第一种画法最后一根线的位置,一定不会在 第二种画法最后一根线 的后面,也就是 opti−1,j≤opti,j o p t i − 1 , j ≤ o p t i , j 。另一半的证明也是类似的
所以我们尝试用这个性质来缩小我们的枚举范围
第一维仍然从
1
1
到 ,第二维从
n
n
到第一维(因为 中,
j
j
总得比 大)。然后第三维,我们从
opti−1,j
o
p
t
i
−
1
,
j
枚举到
opti,j+1
o
p
t
i
,
j
+
1
,特别的,
opti,n+1=n
o
p
t
i
,
n
+
1
=
n
,
opt0,?=0
o
p
t
0
,
?
=
0
。那么这样看起来复杂度是要降低了一点
那么复杂度到底是多少呢?对于 j−i j − i 相同的 i,j i , j ,复杂度为 opti,n+1−opti−1,n+opti−1,n−opti−2,n−1⋯ o p t i , n + 1 − o p t i − 1 , n + o p t i − 1 , n − o p t i − 2 , n − 1 ⋯ ,相邻项抵消,最后剩下 opti,n+1−opt0,n+1−i o p t i , n + 1 − o p t 0 , n + 1 − i ,显然等于 n n 。而 的取值只有 n n 种,所以最后复杂度是 ,可以通过此题
下面是自带大常数的代码
#include <ctime>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;
int N , K , sum[4005][4005] ;
struct Data{
int val , ed , cnt ;
Data( int _ = 0 , int __ = 0 , int ___ = 0 ) : val(_) , ed(__) , cnt(___) {} ;
} que[4005] , dp[4005] ;
struct io
{
#define fs 48000000
char ch[fs];int nr;
io(){ch[fread(ch,1,fs,stdin)]=0;}
inline int read()
{
int x=0;
for(;ch[nr]<48;++nr);
for(;ch[nr]>47;++nr)
x=(x<<1)+(x<<3)+ch[nr]-48;
return x;
}
inline void skip()
{
for(++nr;ch[nr]>10;++nr);
++nr;
}
}io ;
int fr , ba ;
int val( Data x , int now ){
return x.val + sum[now][now] - sum[x.ed][now] ;
}
int cnt ;
int better( Data x , Data y ){
int lf = x.ed , rg = N , rt = N + 1 , mid ;
while( lf <= rg ){
mid = ( lf + rg ) >> 1 ;
int t1 = val( x , mid ) , t2 = val( y , mid ) ;
if( t1 < t2 || ( t1 == t2 && x.cnt < y.cnt ) )
rt = mid , rg = mid - 1 ;
else lf = mid + 1 ;
} return rt ;
}
inline void Push( Data x ){
while( ba > fr ){
int t1 = better( que[ba] , que[ba-1] ) , t2 = better( x , que[ba] ) ;
if( t1 > t2 || ( t1 == t2 && x.cnt <= que[ba].cnt ) ) ba -- ;
else break ;
} que[++ba] = x ;
}
Data calc( int cost ){
fr = 1 , ba = 0 , Push( Data( 0 , 0 , 0 ) ) ;
for( int i = 1 ; i <= N ; i ++ ){
while( ba > fr ){
int t1 = val( que[fr] , i ) , t2 = val( que[fr+1] , i ) ;
if( t1 > t2 || ( t1 == t2 && que[fr].cnt > que[fr+1].cnt ) ) fr ++ ;
else break ;
} dp[i] = Data( val( que[fr] , i ) + cost , i , que[fr].cnt + 1 ) ;
Push( dp[i] ) ;
} return dp[N] ;
}
void solve(){
int lf = 0 , rg = 2000 * 2000 * 10 , ans ;
while( lf <= rg ){
int mid = ( lf + rg ) >> 1 ;
Data res = calc( mid ) ;
if( res.cnt == K )
return ( void )printf( "%d\n" , res.val - K * mid ) ;
if( res.cnt > K ) lf = mid + 1 ;
else ans = mid , rg = mid - 1 ;
} Data res = calc( ans ) ;
printf( "%d\n" , res.val - K * ans ) ;
}
void read_( int &x ){
register char ch = getchar() ;
while( ch < '0' || ch > '9' ) ch = getchar() ;
while( ch >='0' && ch <='9' ) x = ( x << 1 ) + ( x << 3 ) + ch - 48 , ch = getchar() ;
}
int main(){
N = io.read() ; K = io.read() ;
for( int i = 1 ; i <= N ; i ++ ){
int *t1 = sum[i] , *t2 = sum[i-1] ;
for( int j = 1 ; j <= N ; j ++ ){
t1[j] = io.read() ;
if( j <= i ) t1[j] = t2[j] + t1[j-1] - t2[j-1] ;
else t1[j] += t2[j] + t1[j-1] - t2[j-1] ;
}
} solve() ;
}