烽火台又称烽燧,是重要的防御设施,一般建在险要处或交通要道上。一旦有敌情发生,白天燃烧柴草,通过浓烟表达信息:夜晚燃烧干柴,以火光传递军情。在某两座城市之间有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]]; }