poj 3016 K-Monotonic 左偏树+动态规划

又是一个神奇而又优美的数据结构——左偏树

左偏树是什么?左偏树是一棵二叉树,是可合并的堆,它具有“左偏”的性质——简单地说就是左儿子比右儿子“长”,以来维持一定的效率

左偏树能做什么?刚才说了,左偏树是可合并的堆,堆能干的他基本都能干,而合并操作可以达到O(logn)的时间复杂度,而且作为可合并堆,左偏树的实现非常简单

具体的内容就不在这赘述了,百度上有很多更好的资料可以参考

题目大意

将n个数,将其分成k个区间,改变其中的一些数使得每个区间严格单调,求改动的最小代价

这道题和poj 3666 Making the Grade差不太多,3666基本上就相当于是这道题k=1的特殊情况,两题另外的区别就是3666不要求严格单调,这个处理很简单,以求单调升序列为例,对a[i]-i进行操作就行了

将[i, j)区间调整成的单调序列的代价表示为cost[i][j],显然将前i部分分成k块单调所需要的最小代价就是dp[k][i] = min(dp[k][j] + cost[j][i]),很明显的动态规划,剩下的问题就是如何求cost数组了,于是这道题就华丽丽的转变成了3666><

那么如何求一个序列转换成一个单调上升序列的最小代价呢?

1. 最后转换的结果可以用一个依次递增的“柱状图”表示,每一个柱子(下文简称为“一个块”)代表将了这个区间内元素调整为这个柱子高度所表示的值

2. 对于每一个块,如何确定块的高度?或者说,这些元素该调整成一个什么样的值总代价最小?这个问题比较简单,中位数即可,另外在这里,对于偶数中位数可以指中间两个元素的任何一个值

3. 初始的时候每一个块代表一个元素,显然这些块是不符合1中所描述的形象的,需要进行“合并”

4. 合并什么?合并相邻的两个块。如何合并?把他们的元素放到一起,取出新的中位数作为他们的高度。什么时候合并?如果后一个块比前一个块高,那么是符合要求的,不进行合并;如果低,就进行合并。(至于为什么要进行这样的操作可以用调整的方法稍微想一下)

5. 调整(合并)完所有的块就达成了1中描述的样子,这时每一个块的代价和就是最终结果

经过了上面的这些分析之后我们就可以有大概的算法框架了:初始所有的块都为单个元素,顺序扫描这些块,每次都这个块的中位数与前一块比较,如果小,进行合并,直到不能合并为止,扫描完所有块时,算法结束。

于是现在问题的核心就是如何记录中位数了,如果每次合并两个块都要扫描一下新块中所有的元素,那显然是很费时间的,那么就轮到今天的主角——左偏树登场了

首先必须要说明的是,左偏树通常是不能用来维护中位数的,这里能用是特殊情况

如何合并呢?左偏树是个堆嘛,维护一个区间内的前一半元素就好了,这样堆顶的元素肯定就是中位数了,合并的时候看一看合并之后的元素是不是超过了总元素的一半,如果超过了就把堆顶元素弹出去,新值就是新块的中位数。

那么有同学可能就要问了,树中只存了前一半的元素,合并之后可能会出问题嘛,比如说一个序列是5678,另一个是1234,只存一半的话合并的结果就是1256了,显然中位数不对。这里就是这道题的特殊情况了,就这里讲,上述的这种情况是不存在的,因为后面那个序列中被踢掉的元素一定比前面那个块的当前中位数大。为什么?我们先假设前面的块都是按中位数大小已经排好的,下面我们要对一个新的只有一个元素的块进行合并,不要忘记了,第一,只有当前块大于后块时才会合并,第二,被踢元素是因为当前维护的元素超过了所代表的区间的一半,而每次合并的都是一半的元素,所以每次最多只踢一个,那么这次合并哪个被踢了呢?显然是原先前面块的中位数,那么如果之后又发生了一次合并,刚才担心的问题是踢掉的数比中位数小,由假设,之前处理完块已经按中位数大小排好,那么后面块的中位数(被踢掉的那个)一定比前面大,这样就保证了维护的中位数是正确的了。

还有最后一个问题,每一次算一个cost都是O(nlogn)的,如果枚举每个起点i和终点j,那么总复杂度是O(n^3logn),显然是不可接受的,而实际上我们在扫描的过程中每处理完一个块,实际上当前起点到这个点的cost值就算出来了,也就是说只要枚举起点就够了,这样总复杂度O(n^2logn),问题圆满解决~


代码(这道题树节点一定要用数组存,动态new可能会超时,原先使用new写的,这里改的比较省事,所以有一些适用于new写法的代码也就没删==)

// 左偏树维护中位数+动归
#include <cstdio>
#include <cstring>
#include <iostream>
#define MAXN 2000
using namespace std;


struct treeNode     // 左偏树节点
{
    treeNode * l;
    treeNode * r;
    int len;
    int key;
    treeNode(int val): l(NULL), r(NULL), len(0), key(val){}
    treeNode(){}
}tr[MAXN];
int cnt;


treeNode * merge(treeNode * a, treeNode * b) // 左偏树合并
{
    if (a == NULL) return b;
    if (b == NULL) return a;
    if (a->key < b->key) swap(a, b);
    a->r = merge(a->r, b);
    if (a->l == NULL || a->l->len < a->r->len) swap(a->l, a->r);
    if (a->r == NULL) a->len = 0;
    else a->len = a->r->len + 1;
    return a;
}


treeNode * delMax(treeNode * a)     // 删除最大元素
{
    treeNode * tmp = a;
    a = merge(a->l, a->r);
    //delete tmp;
    return a;
}


treeNode * newNode(int val)
{
    return new treeNode(val);
}


struct wset     // 保存每一个“块”
{
    treeNode * p;   // 代表该块的树根节点
    int tot;        // 该块所代表的元素个数
}w[MAXN];


// 处理a数组,元素个数为n,将前i个调整成单调序列的cost存放到ans[i]中
void work(int * a, int n, int * ans)
{
    int top = 0;
    int ts = 0; // ts表示当前处理完的区间的cost
    cnt = 0;
    memset(tr, 0, sizeof(tr));
    for (int i = 0; i < n; ++i)
    {
        w[top].p = &tr[cnt];    // 建立新的树节点和块
        tr[cnt++].key = a[i];
        w[top].tot = 1;
        top++;
        // 当至少还有两个块而且前块比后块中位数大时进行合并操作
        while (top > 1 && w[top - 2].p->key > w[top - 1].p->key)
        {
            int tmp = w[top - 2].p->key;
            w[top - 2].p = merge(w[top - 1].p, w[top - 2].p);
            // 块中偶数个元素保存前n/2个,所以两个块合并应该删除一个元素
            // 其实保存前n/2+1个元素也可以
            // 在每一次合并的过程中至多只有两个元素对cost的变化有贡献,因为中位数的变化在一定的区间范围内
            // 对于前块来说,中位数可能会减少,则元素个数为奇数的块的原中位数对ts有贡献
            // 对于后块来说,当元素个数为奇数时,原中位数对ts有贡献
            if (w[top - 2].tot % 2 == 1 && w[top - 1].tot % 2 == 1)
            {
                w[top - 2].p = delMax(w[top - 2].p);
                ts += tmp - w[top - 2].p->key;
            }
            if (w[top - 1].tot % 2 == 1)
            {
                ts += w[top - 2].p->key - w[top - 1].p->key;
            }
            w[top - 2].tot += w[top - 1].tot;
            top--;
        }
        ans[i] = ts;
    }
}


int a[MAXN], b[MAXN], c[MAXN], cost[2][MAXN][MAXN], dp[MAXN][MAXN];


int main()
{
    int n, k;
    while (1)
    {
        scanf("%d%d", &n, &k);
        if (n == 0) break;
        for (int i = 0; i < n; ++i)
        {
            scanf("%d", a + i);
            b[i] = a[i] - i;
            c[i] = -a[i] - i;
        }


        memset(cost, 0x3F, sizeof(cost));
        for (int i = 0; i < n; i++)
        {
            work(b + i, n - i, cost[0][i] + i);
            work(c + i, n - i, cost[1][i] + i);
        }


        // DP求解
        memset(dp, 0x3F, sizeof(dp));
        dp[0][0] = 0;
        int i, j, t;
            
        for (t = 1; t <= k; ++t)
        {
            for (i = 1; i <= n; ++i)
            {
                for (j = 0; j < i; ++j)
                {
                    dp[t][i] = min(dp[t][i], dp[t - 1][j] + min(cost[0][j][i - 1], cost[1][j][i - 1]));
                }
            }
        }


        printf("%d\n", dp[k][n]);
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值