【问题标题】:Optimize: Divide an array into continuous subsequences of length no greater than k such that sum of maximum value of each subsequence is minimum优化:将一个数组分成长度不大于k的连续子序列,使得每个子序列的最大值之和最小
【发布时间】:2017-05-09 04:46:34
【问题描述】:

优化O(n^2)算法为O(n log n)

问题陈述

给定数组An 正整数。将数组划分为长度不大于k 的连续子序列,使每个子序列的最大值之和最小。这是一个例子。

如果n = 8k = 5 和数组的元素是1 4 1 3 4 7 2 2,最好的解决方案是1 | 4 1 3 4 7 | 2 2。总和将是max{1} + max{4, 1, 3, 4, 7} + max{2, 2} = 1 + 7 + 2 = 10

O(n^2) 解

dp[i] 成为子问题数组A[0] ... A[i] 的问题陈述中的最小总和。 dp[0] = A[0]0 < i < n (dp[-1] = 0),

// A, n, k, - defined
// dp - all initialized to INF
dp[0] = A[0];
for (auto i = 1; i < n; i++) {
    auto max = -INF;
    for (auto j = i; j >= 0 && j >= i-k+1; j--) {
        if (A[j] > max)
            max = A[j];
        auto sum = max + (j > 0 ? dp[j-1] : 0);
        if (sum < dp[i])
            dp[i] = sum;
    }
}
// answer: dp[n-1]

O(n log n) ?

问题作者声称可以在O(n log n)时间解决这个问题,并且有一些人能够通过测试用例。如何优化?

【问题讨论】:

  • 您当前的算法复杂度为 O(n*k),因为第二个循环回溯的时间不超过k 步。
  • 在找到最小化 dp[i] 的 j 之后,如果结果证明这个 j 导致最终段的元素严格少于 k 个,那么这个 j 也将给出 dp[ 的有效解我+1]。令 m 为 dp[i] 的最优解的最后一段中的最大元素。 A[i+1] 可以是 > m 或
  • 想知道这个问题在现实世界中的示例应用,有什么想法吗?

标签: algorithm dynamic-programming


【解决方案1】:

注意:我会稍微改变你的动态规划关系,这样j = 0就没有特殊情况了。现在dp[j] 是第一个j termsA[0], ..., A[j-1] 的答案,并且:

dp[i] = min(dp[j] + max(A[j], ..., A[i-1]), i-k &lt;= j &lt; i)

问题的答案现在是dp[n]


请注意,如果j &lt; idp[j] &gt;= dp[i],则在以下转换中您将不需要dp[j],因为max(A[j], ..., A[l]) &gt;= max(A[i], ..., A[l])(因此在i 而不是j 处剪切总是更好。

此外,让C[j] = max(A[j+1], ..., A[l])(其中l 是我们在动态编程步骤中的当前索引,即C++ 程序中的i)。

然后您可以在内存中保留一组索引x1 &lt; ... &lt; xm(动态规划关系转换的“有趣”索引),例如:dp[x1] &lt; ... &lt; dp[xm] (1)。然后自动C[x1] &gt;= ... &gt;= C[xm](2)。

要存储{x1, ..., xm},我们需要一些支持以下操作的数据结构:

  • 弹回(当我们从i 移动到i+1 时,我们必须说i-k 现在无法访问)或前面(参见插入)。
  • 推前x(当我们计算出dp[i]时,我们在保留(1)的同时插入它,通过删除相应的元素)。
  • 计算min(dp[xj] + C[xj], 1 &lt;= j &lt;= m)

因此,一些队列来存储x1, ..., xk 以及一个set 来存储所有dp[xi] + C[xi] 就足够了。


当我们插入元素 i 时,我们如何既保留 (1) 又更新 C

  • 在计算dp[i] 之前,我们将C 更新为A[i-1]。为此,我们在集合x s.t. 中找到最小元素xjC[xj] &lt;= A[i-1]。然后 (1) 和 (2) 对所有 j' &gt;= j 隐含 dp[j'] + C[j'] &gt;= dp[j] + C[j],因此我们将 C[xj] 更新为 A[i-1] 并从集合 (*) 中删除 x(j+1), ..., xm
  • 当我们插入dp[i] 时,我们只是删除所有元素 s.t. dp[j] &gt;= dp[i] 弹出前面。
  • 当我们删除 i-k 时,可能在 (*) 中破坏的某些元素现在变得最好了。因此,如有必要,我们会更新 C 并插入最后一个元素。

复杂度:O(n log n)(集合中最多可能有 2n 插入)。

这段代码总结了主要思想:

template<class T> void relaxmax(T& r, T v) { r = max(r, v); }

vector<int> dp(n + 1);
vector<int> C(n + 1, -INF);
vector<int> q(n + 1);
vector<int> ne(n + 1, -INF);
int qback = 0, qfront = 0;
auto cmp = [&](const int& x, const int& y) {
    int vx = dp[x] + C[x], vy = dp[y] + C[y];
    return vx != vy ? vx < vy : x < y;
};
set<int, decltype(cmp)> s(cmp);

dp[0] = 0;
s.insert(0);
q[qfront++] = 0;

for (int i = 1; i <= n; ++i) {
    C[i] = A[i - 1];
    auto it_last = lower_bound(q.begin() + qback, q.begin() + qfront, i, [=](const int& x, const int& y) {
        return C[x] > C[y];
    });

    for (auto it = it_last; it != q.begin() + qfront; ++it) {
        s.erase(*it);
        C[*it] = A[i - 1];
        ne[*it] = i;
        if (it == it_last) s.insert(*it);
    }

    dp[i] = dp[*s.begin()] + C[*s.begin()];

    while (qback < qfront && dp[q[qfront]] >= dp[i]) {
        s.erase(q[qfront]);
        qfront--;
    }

    q[qfront++] = i;
    C[i] = -INF;
    s.insert(i);

    if (q[qback] == i - k) {
        s.erase(i - k);

        if (qback + 1 != qfront && ne[q[qback]] > q[qback + 1]) {
            s.erase(q[qback + 1]);
            relaxmax(C[q[qback + 1]], C[i - k]);
            s.insert(q[qback + 1]);
        }

        qback++;
    }
}

// answer: dp[n]

这次我根据你的算法对它进行了压力测试:see here

如果还不清楚,请告诉我。

【讨论】:

  • 您的代码似乎在我的问题中给出的示例中返回 14。我不确定我是否理解您的操作正确,但这就是我的做法:Link。此外,我在此代码中的索引 1 和 C[j] = max{A[j], ..., A[i]} 处开始 AdpC。我不确定您如何处理财产 (2),因此我希望对此进行一些澄清。 (&gt;= dp[i] 仅此处的测试消除使我的解决方案通过了测试用例,但出于好奇。谢谢!)
  • @AquaBlitz11:我更新了我的答案。现在它应该可以工作了(请注意,您不需要multiset,因为这里的所有元素都是不同的)。
猜你喜欢
  • 2019-08-22
  • 2022-12-20
  • 2016-12-20
  • 2021-02-18
  • 2023-03-27
  • 2019-10-13
  • 1970-01-01
  • 2023-03-04
  • 1970-01-01
相关资源
最近更新 更多