决策单调性与分治: 避雷针

SP9070&P3515 避雷针

直通车

前言

做 DP 最爽的不是 AC,而是不断地优化程序,每次达到更高的效率,都能收获非常浓烈的成就感。接下来的每代码, 至少能在某个 OJ 上 AC, 所以没有错误解法, 只有效率优劣之分.

题意

一个序列 a,求所有最小的自然数 fi 使得 fi+aiaj+|ij|

O(nn)

这个题看起来像是 DP,但是式子里面有根号还是很不常见的。 有过几次根号分治的经验后,我觉得可以根号分治。

整理式子,fi+ai|ij|aj,这时如果能每一个 |ij| 值对应的应该是关于 i 对称的连续区间,我们只要使得 fi+ai|ij| 的值大于等于这个区间的最大值即可,而这种区间的数量是 O(n) 个,提前 O(nlogn) 预处理出 ST 表后,区间查询复杂度是 O(1),总复杂度 O(nn),看了看 5105,有概率能过。

虽然在 Libre OJ 上在 -Ofast 加持下拿到了 AC,在洛谷上开 -O2 却只有 70,只比不开 -O2 多了 10。 因为 ST 表的常数比那个同为 O(nn) 的题解差的太多,所以无论如何也卡不到 70 以上。

(接近) 极限卡常代码:

unsigned ST[20][500005], Square[1005], Bin[20], Log[500005], m, n;
const unsigned _1(1);
inline unsigned Qry(unsigned L, unsigned R) {
  register unsigned LogLen(Log[R - L + _1]);
  return std::max(ST[LogLen][L], ST[LogLen][R - Bin[LogLen] + 1]);
}
int main() {
  n = RD();
  for (register unsigned i(_1); i <= n; ++i) ST[0][i] = RD();
  for (register unsigned i(_1), j(0); i <= n; i <<= 1, ++j) Bin[j] = i, Log[i] = j;
  for (register unsigned i(_1); i <= n; ++i) Log[i] = std::max(Log[i], Log[i - _1]);
  for (register short i(_1); Square[i - _1] <= n; ++i) Square[i] = i * i;
  for (register unsigned i(_1); i <= Log[n]; ++i)
    for (register unsigned j(_1); j + Bin[i - _1] <= n; ++j) 
      ST[i][j] = std::max(ST[i - _1][j], ST[i - _1][j + Bin[i - _1]]);
  for (register unsigned i(_1), j, Tmp, Ans; i <= n; ++i) {
    Ans = ST[0][i];
    if(i ^ _1) {
      j = _1, Tmp = i - _1;
      for (; Square[j] < i - _1; Tmp = i - Square[j] - _1, ++j)
        Ans = std::max(Ans, j + Qry(i - Square[j], Tmp));
      Ans = std::max(Ans, j + Qry(_1, Tmp));
    }
    if(i ^ n) {
      j = _1, Tmp = i + _1;
      for (; i + Square[j] < n; Tmp = i + Square[j] + _1, ++j)
        Ans = std::max(Ans, j + Qry(Tmp, i + Square[j]));
      Ans = std::max(Ans, j + Qry(Tmp, n));
    }
    printf("%u\n", ((Ans < ST[0][i]) ? 0 : Ans - ST[0][i]));
  }
  return Wild_Donkey;
}

O(nn)-改

重新审视这个约束式子:

fi+aiaj+|ij|

我们发现如果要求 fi,只需要求 fi+ai 即可,而这样我们需要知道所有 j[1,n]aj+|ij| 的最大值,分为两种情况来求:

  • j[1,i]

  • j(i,n]

拿第一种情况举例子,因为问题对称所以不失一般性,我们可以在第二种情况使用同样的方法。

我们发现 i 变成 i+1aj+|ij| 的值会有 i 个发生改变 (+1),所以每次 i 的移动我们在原数组的副本中修改这 i 个值,与此同时维护最大值即可。

对于 i+1 来说,最大值可以由 i 那里继承过来,因为所有 j[1,i] 的值在 i+1 这一轮都不会变小,而且变大的情况我们在维护时就考虑了,唯一一个增加元素 j=i+1 也可以 O(1) 统计。

就在我绞尽脑汁优化把我的技能榨干还只能在 -Ofast 加持下在 Libre OJ 上超时挂到 76' 的时候,我在洛谷就以相同的代码仅开 -O2 以极高的效率 AC 了此题,只用了 1.85s,不过关上 -O2 直接 TLE 到 60

即使如此,SPOJ 把时间丧心病狂地开到了 205ms,一个 O(nn) 的算法铁定是过不了的......吗?

但是上面的代码过于头铁还是 AC 了,甚至只用了 3.70s 就过了 20 个点 (又交了一发 3.65s,发现 SP 上时限是 1s,洛谷管理有看到的更新一下 Remote Judge 信息),最离谱的是 SP 没有 -O2 选项,真是太恐怖了。 (可能是因为我晚上交的,这时候西方貌似是上午,而清晨一般是评测机最闲的时候)

所以接下来短短 23 行,你们将看到一个工程学奇迹,这是实实在在顶着上限跑,一点枝没剪,暴力地枚举 5e8 只用 500ms 的怪物 (2nn):

unsigned a[500005], Fst[500005], Scd[500005], Square[1005], Ans[500005], m, n;
signed main() {
  n = RD();
  for (register unsigned i(0x1); i <= n; ++i) a[i] = RD();
  for (register unsigned i(0x1); Square[i - 0x1] <= n; ++i) Square[i] = i * i;
  memcpy(Fst, a, (n + 0x1) << 0x2);
  for (register unsigned i(0x1), j; i <= n; ++i) {
    Scd[i] = std::max(Scd[i - 0x1], Fst[i]), j = 0x0;
    for (; Square[j] + 0x1 < i; ++j)
      Scd[i] = std::max(Scd[i], ++Fst[i - Square[j] - 0x1]);
  }
  memcpy(Fst, a, (n + 0x1) << 0x2);
  memcpy(Ans, Scd, (n + 0x1) << 0x2);
  for (register unsigned i(n), j; i; --i) {
    Scd[i] = Scd[i + 0x1], j = 0x0;
    for (; i + Square[j] < n; ++j)
      Scd[i] = std::max(Scd[i], ++Fst[i + Square[j] + 0x1]);
    Ans[i] = std::max(Scd[i], Ans[i]);
  }
  for (register unsigned i(0x1); i <= n; ++i)
    printf("%u\n", Ans[i] - a[i]);
  return Wild_Donkey;
}

跳着阅读的同学注意了,如果你希望通过这道题,那么你可以止步于此了,因为上面的代码最慢的点可以在时间限制的一半左右跑完。

O(nn)-Pro

仍然分析这两种情况:

  • j[1,i]

    假设这时取到 aj+ij 的最大值的 j=k,则 aj+ij+1 最大值的 jk,这是因为对于越接近 ijij 的大小随 i 的增长而增长的速率越快,所以更大的 jij 增长更快,才可能成为最大值。

  • j(i,n]

    同样地,对于使得 aj+ji+1 取最大值的 j 一定不大于使得 aj+ji 取最大值的 j

对于同一个 j<i,如果有 i>i,那么一定有 |ij|<|ij|。这时可以断定,对于在 i 左边的所有 jaj+|ij| 的最大值一定不比 aj+|ij| 大。

同样的,如果有 i<i,那么对一切 j>i,一定有 aj+|ij| 的最大值一定不比 aj+|ij| 大。

这就是我们所说的决策单调性。

剪枝,枚举时维护一个指针 But,每次从这里作为维护的边界,虽说复杂度没变,但是应付随机数据大概率能比 O(nlogn) 跑满都快。

加上剪枝,LOJ 还是过不了,不过拿了 86,多了 10,但是洛谷这边总时间优化到了 1.4s,最慢的点已经能控制在时限一半以下了。

unsigned a[500005], Fst[500005], Scd[500005], Square[1005], Ans[500005], m, n;
signed main() {
  n = RD();
  for (register unsigned i(0x1); i <= n; ++i) a[i] = RD();
  for (register unsigned i(0x1); Square[i - 0x1] <= n; ++i) Square[i] = i * i;
  memcpy(Fst, a, (n + 0x1) << 0x2);
  for (register unsigned i(0x1), j, But(1); i <= n; ++i) {
    Scd[i] = std::max(Scd[i - 0x1], Fst[i]), j = 0x0;
    for (; But + Square[j] < i; ++j)
      if(Scd[i] < ++Fst[i - Square[j] - 0x1])
        But = i - Square[j] - 0x1, Scd[i] = Fst[But];      
    if(Fst[i] == Scd[i]) But = i;
  }
  memcpy(Fst, a, (n + 0x1) << 0x2);
  memcpy(Ans, Scd, (n + 0x1) << 0x2);
  for (register unsigned i(n), j, But(n); i; --i) {
    Scd[i] = Scd[i + 0x1], j = 0x0;
    for (; i + Square[j] < But; ++j)
      if(Scd[i] < ++Fst[i + Square[j] + 0x1])
        But = i + Square[j] + 0x1, Scd[i] = Fst[But];
    Ans[i] = std::max(Scd[i], Ans[i]);
  }
  for (register unsigned i(0x1); i <= n; ++i)
    printf("%u\n", Ans[i] - a[i]);
  return Wild_Donkey;
}

O(nlogn)

虽然能过,但是复杂度终究是王道,接下来参考这篇题解,介绍一下 O(nlogn) 的做法。

根据前面得到的决策单调性,我们可以分治来 O(nlogn) 地解决本题,仍然只讨论左边的情况。

规定一个状态 i 的决策是对于 j[1,i) 使得 aj+|ij| 取最大值的 j

假设这时已知 L 的决策 lR 的决策 r,这时对于所有 i[L,R],他们的决策 j 一定满足 [l,r]

这时我们取 LR 的中点 MidO(n) 枚举所有 j[l,r] 求出 Mid 的决策 m,这时就可以向下递归下去了。

由主定理得总复杂度 O(nlogn)

unsigned a[500005], SqRoot[500005], Ans[500005], m, n;
double Max[500005];
char Flg(0); 
void Merge(unsigned L, unsigned R, unsigned DL, unsigned DR) {
  if(L == R) {
    if(Flg) {
      DL = std::max(L, DL);
      for (register unsigned i(DL); i <= DR; ++i)
        Max[L] = std::max(Max[L], a[i] + sqrt(i - L));
    } else {
      DR = std::min(L, DR);
      for (register unsigned i(DL); i <= DR; ++i)
        Max[L] = std::max(Max[L], a[i] + sqrt(L - i));
    }
    Ans[L] = std::max((unsigned)(Max[L] + 0.999999), Ans[L]);
  } else {
    register unsigned Mid((L + R) >> 0x1), mid, Border;
    if(Flg) {
      Border = std::max(Mid, DL);
      for (register unsigned i(Border); i <= DR; ++i)
        if(Max[Mid] < a[i] + sqrt(i - Mid))
          Max[Mid] = a[i] + sqrt(i - Mid), mid = i;
    } else {
      Border = std::min(Mid, DR);
      for (register unsigned i(Border); i >= DL; --i)
        if(Max[Mid] < a[i] + sqrt(Mid - i))
          Max[Mid] = a[i] + sqrt(Mid - i), mid = i;
    }
    Ans[Mid] = std::max((unsigned)(Max[Mid] + 0.999999), Ans[Mid]);
    if(L ^ Mid) Merge(L, Mid - 0x1, DL, mid);
    Merge(Mid + 0x1, R, mid, DR);
  }
}
signed main() {
  n = RD();
  for (register unsigned i(0x1); i <= n; ++i) a[i] = RD();
  for (register unsigned i(0x1); i * i <= n; ++i) SqRoot[i * i + 0x1] = i + 0x1;
  for (register unsigned i(0x1); i <= n;++i) SqRoot[i] = std::max(SqRoot[i - 0x1], SqRoot[i]);
  Merge(0x1, n, 0x1, n);
  memset(Max, 0x00, (n + 0x1) << 0x3);
  Flg = 0x1;
  Merge(0x1, n, 0x1, n);
  for (register unsigned i(0x1); i <= n; ++i)
    printf("%u\n", Ans[i] - a[i]);
  return Wild_Donkey;
}

洛谷不开 -O2 的总时间是 547ms,非常大的进步。

后记

这时我 AC 的第 3031 道紫题 (可能更多,因为不少紫题掉蓝了),也是我在洛谷做的第 332333 道题,最后给出双倍经验

posted @   Wild_Donkey  阅读(178)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示