[BZOJ5311]-[codeforces321E]Ciel and Gondolas-dp凸优化 / dp决策分治 / 类四边形优化

说在前面

肝这题肝了几天
边比赛边肝题,终于肝出来了…开心qwq


题目

UPD:此题题源为codeforces321E
BZOJ5311传送门

题目大意

给定一个长度为 n n 的序列,现在你需要将这个序列划分成连续的 k
被划分到同一段的两个位置 i,j i , j ,会产生代价 aij a i j ,不同段不会产生代价
现在请求出最小的贡献

范围: n4000 n ≤ 4000 k800 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 ,含义如题
接下来是一个 nn n ∗ n 的矩阵,第 i i 行第 j 列的矩阵代表 aij a i j

输出格式:
输出一行一个整数,表示答案


解法

这个题,me在BZOJ上T了一版,后来换了个方法终于过了
(不知道是不是me想复杂了,但me觉得这是一个很好的题

算法0

首先,我们可以写出一个很显然的DP
定义 dpi,j d p i , j 表示,前 j j 个分 i 组的最小代价
转移 dpi,j=min{dpi1,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 并求前缀和,转移就可以写成这样
dpi,j=min{dpi1,k+sum[j][j]sum[k][j]}
复杂度 n2k n 2 k

算法1

考虑对算法1进行优化
我们发现,随着 j j 的变大,靠前的转移点会慢慢变得不优。具体的,转移具有单调性这里写图片描述如图,如果从 k 转移,代价为 s1+s2 s 1 + s 2 ,如果从 k k ′ 转移,代价为 s2 s 2 。因为代价都是正的,所以从 k k 转移的代价不会小于从 k 转移的代价,并且差值 s1 s 1 可能越来越大
然而,由于 dpi1,k<dpi1,k d p i − 1 , k < d p i − 1 , k ′ ,因此并没有显然的单调性
但我们知道,一旦在某刻时刻, k k ′ k k 优,那么它将永远比 k

所以,我们可以用一个单调队列来维护这些转移点。保证单调队列里 后一个点超越前一个点的时间单调即可。这也是 [JSOI2011]柠檬 的一种可行做法
时间复杂度 nk log2n n k   log 2 n

算法2

我们发现时限太小了!只有3s,而且读入数据高达 4106 4 ∗ 10 6 个整数,算法2仍然是不可过的,但是由于me没有想出进一步的优化,于是考虑换思路

重新审视这道题, n n 的数列,恰好 k 段,而且随着 k k 的减小,答案会越来越大,并且δk=ansk1ansk,这个 δk δ k 也越来越大
这启发我们想到dp凸优化(又称带权二分 / WQS 二分)
这是一类二分方法,专门用于限制「恰好 k k 个」且「δi单调」的题目中

复杂度 n log2n log2aij n   log 2 n   log 2 ∑ a i j ,加上fread可通过此题

算法3

这是CF上原题的解法传送门
还是记 dpi,j d p i , j 表示前 j j 个分 i 组的最小代价
然后把 dpi,j d p i , j 最优转移的最小位置记作 opti,j o p t i , j ,即若 dpi,j=dpi1,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 相同的时候,显然有opti,1opti,2opti,n
根据这一性质,我们考虑分治,我们花费 O(n) O ( n ) 先求出 dpi,mid d p i , m i d opti,mid o p t i , m i d ,这样,我们就可以知道 dpi,1...mid1 d p i , 1... m i d − 1 的决策点都不会超过 opti,mid o p t i , m i d mid+1n 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 ,而相邻决策区间最多重合 1 (就是重合了 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 的最优转移点,那么可以发现一个性质: opti1,jopti,jopti,j+1 o p t i − 1 , j ≤ o p t i , j ≤ o p t i , j + 1

我们来感性理解一下这个性质: dpi1,j d p i − 1 , j 可以看作是,在前 j j 个里画 i1 根线使其尽量「平均」, opti1,j o p t i − 1 , j 相当于是最后一根线的位置。同理, dpi,j d p i , j 是在前 j j 个里画 i 根线, opti,j o p t i , j 是最后一根线的位置。因为要「平均」,所以 第一种画法最后一根线的位置,一定不会在 第二种画法最后一根线 的后面,也就是 opti1,jopti,j o p t i − 1 , j ≤ o p t i , j 。另一半的证明也是类似的

所以我们尝试用这个性质来缩小我们的枚举范围
第一维仍然从 1 1 k,第二维从 n n 到第一维(因为 dpi,j 中, j j 总得比 i 大)。然后第三维,我们从 opti1,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 。那么这样看起来复杂度是要降低了一点

那么复杂度到底是多少呢?对于 ji j − i 相同的 i,j i , j ,复杂度为 opti,n+1opti1,n+opti1,nopti2,n1 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+1opt0,n+1i o p t i , n + 1 − o p t 0 , n + 1 − i ,显然等于 n n 。而 ji 的取值只有 n n 种,所以最后复杂度是 n2,可以通过此题


下面是自带大常数的代码

#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() ;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值