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

说在前面

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


题目

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

题目大意

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

范围: n 4000 k 800 ,代价矩阵 a i j 的数字均在 [ 0 , 10 ] 之间
保证: a i j = a j i , a i i = 0
要求:一对 ( i , j ) 的代价只计算一次

输出格式

输入格式:
第一行两个整数 n , k ,含义如题
接下来是一个 n n 的矩阵,第 i 行第 j 列的矩阵代表 a i j

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


解法

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

算法0

首先,我们可以写出一个很显然的DP
定义 d p i , j 表示,前 j 个分 i 组的最小代价
转移 d p i , j = min { d p i 1 , k + c o s t [ j + 1 ] [ k ] }

如果我们把代价矩阵 i > j 的部分变成 0 并求前缀和,转移就可以写成这样
d p i , j = min { d p i 1 , k + s u m [ j ] [ j ] s u m [ k ] [ j ] }
复杂度 n 2 k

算法1

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

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

算法2

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

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

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

算法3

这是CF上原题的解法传送门
还是记 d p i , j 表示前 j 个分 i 组的最小代价
然后把 d p i , j 最优转移的最小位置记作 o p t i , j ,即若 d p i , j = d p i 1 , k + c o s t [ k + 1 ] [ j ] ,则 o p t i , j = k

那么在 i 相同的时候,显然有 o p t i , 1 o p t i , 2 o p t i , n
根据这一性质,我们考虑分治,我们花费 O ( n ) 先求出 d p i , m i d o p t i , m i d ,这样,我们就可以知道 d p i , 1... m i d 1 的决策点都不会超过 o p t i , m i d 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 ,而相邻决策区间最多重合 1 (就是重合了 o p t d , m i d ),所以一层的复杂度上限 2 n ,一共log层,所以单次复杂度 n log n ,总复杂度 n k log 2 n

卧槽为什么我写完了才发现这个方法的复杂度也那么大??还是过不去BZOJ上那个神tm时限

算法4

这个方法很有意思,me还没有仔细研究,不过感觉推广性挺强的
这个题的最优决策,其实可以看作是把这个序列分的尽量「平均」。「平均」是指,每一段的代价尽量平均

我们还是定义 o p t i , j d p i , j 的最优转移点,那么可以发现一个性质: o p t i 1 , j o p t i , j o p t i , j + 1

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

所以我们尝试用这个性质来缩小我们的枚举范围
第一维仍然从 1 k ,第二维从 n 到第一维(因为 d p i , j 中, j 总得比 i 大)。然后第三维,我们从 o p t i 1 , j 枚举到 o p t i , j + 1 ,特别的, o p t i , n + 1 = n o p t 0 , ? = 0 。那么这样看起来复杂度是要降低了一点

那么复杂度到底是多少呢?对于 j i 相同的 i , j ,复杂度为 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 ,相邻项抵消,最后剩下 o p t i , n + 1 o p t 0 , n + 1 i ,显然等于 n 。而 j i 的取值只有 n 种,所以最后复杂度是 n 2 ,可以通过此题


下面是自带大常数的代码

#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() ;
}

猜你喜欢

转载自blog.csdn.net/izumi_hanako/article/details/80275299
今日推荐