单调队列之烽火传递问题

烽火台又称烽燧,是重要的防御设施,一般建在险要处或交通要道上。一旦有敌情发生,白天燃烧柴草,通过浓烟表达信息:夜晚燃烧干柴,以火光传递军情。在某两座城市之间有n个烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确的传递,在m个烽火台中至少要有一个发出信号。现输入n,m和每个烽火台发出的信号的代价,请计算总共最少需要话费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确的传递。

 例如,有5个烽火台,它们发出信号的代价为1,2,5,6,2,且m为3,则总共最少花费的代价为4,即由第2个和第5个烽火台发出信号。


先翻译成数学题,给定n个正数的序列A1,A2, A3,...., An,定义m子序列Ai1,Ai2, Ai3,...., Aik,该子序列的任意两个相邻元素的下标差不超过m(i2-i1<=m...),而且i1-1<m,n-ik<m,求和最小的m子序列。

 

此题网上也有答案,无奈都是颇为简单,个人能力有限实在难以理解,故成此文。

最关键的是抽象出状态转移方程,令F[i] 表示前i个正数序列的最小和m子序列,并且选中最后一个元素(第i个元素),注意选中最后一个元素非常关键,之前个人无法理解就是因为没有认识到这一点。可以看到,n个正数序列的最小和m子序列必定以某个数结尾,所以此m子序列的最后一个数一定是An-m+1..... An其中之一,即最小和为MIN(F[n-m+1]...F[n])

动态规划

根据以上的定义,可以很轻松的写出F[i]的状态转移方程,如下:

F[i] = Ai +MIN(F[i-m]...F[i-1])

其代码如下,复杂度为O(n^2):

static unsigned long long discrete_sum_dp(int *s, int n, int m)
{
    int i, j;
    unsigned long long *F = NULL, min = -1;

    F = malloc(sizeof(unsigned long long) * (n + 1));
    memset(F, -1, sizeof(unsigned long long) * (n + 1));

    F[0] = 0;
    for (i = 1;i <= n;i++) {
        for (j = i - 1;j >= 0 && j >= i - m;j--) {
            if (F[j] + s[i - 1] < F[i])
                F[i] = F[j] + s[i - 1];
        }
    }

#ifdef DEBUG
    for (i = 0;i <= n;i++)
        printf("F[%d] = %lld\n", i, F[i]);
#endif

    for (i = n;i >= n - (m - 1);i--) {
        if (min > F[i])
            min = F[i];
    }
    
    return min;
}

 

单调队列

观察其动态规划时的状态转移方程,发现其关键点是获取区间内的最小值,注意这个区间是动态移动的,这一类问题都可以使用单调队列完成。

使用数组P存储F的下标,而且数组P单调递增,即对于任意i,j属于P,i<j则有F[i]<=F[j],即数组P的头元素永远最小,而且保证数组P中最多存储最新的m个元素。

代码细节:

1. 每检查一个元素时,可以立刻获取F[i],即F[i] = F[P[start]] + s[i-1],因为数组P的头元素永远最小。

2. 从队列尾部开始检查是否大于F[i],将数组P中所有大于F[i]的元素出队列,因为F[i]比这些出队列的元素都小,所以在获取区间最小值时不需要考虑比F[i]大而且比F[i]旧的元素。

3. 从队列首部开始检查,确保数组P中最多m个元素

代码如下:

static unsigned long long discrete_sum_pq(int *s, int n, int m)
{
    int i;
    int start = 0, end = -1;     /* priority queue pointer */
    unsigned long long *F = NULL;
    int *P = NULL;

    F = malloc(sizeof(unsigned long long) * (n + 1));
    P = malloc(sizeof(unsigned int) * (n + 1));
    
    memset(F, -1, sizeof(int) * (n + 1));

    F[0] = 0;
    P[++end] = 0;
    for (i = 1;i <= n;i++) {
        F[i] = F[P[start]] + s[i - 1];
        for (;end >= start && F[P[end]] > F[i];)
            end--;
        end++;
        P[end] = i;

        for (;start <= end && (i - P[start]) >= m;)
            start++;
    }

#ifdef DEBUG
    for (i = 0;i <= n;i++)
        printf("F[%d] = %lld\n", i, F[i]);
    for (i = start;i <= end;i++)
        printf("P[%d] = %d\n", i, P[i]);
#endif

    return F[P[start]];
}



评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值