博弈

模拟 博弈论

思路分析

根据题意,选择的两个元素若是一奇一偶那么最终值相对于它们的和将会减少 ,否则不变。所以对于第一个人,肯定是优先选择同奇同偶的。第二个人需要最小化答案,那么会优先选择一奇一偶的。

问题

但是第一个人在奇数和偶数都很多的情况下,会进行什么操作呢?它会优先选择两个奇数的消掉。这样就可以让第二个人没有奇数用;不消偶数的原因是不管任何情况合出来都会增加一个偶数。

正确思路

所以先进行若干次理想型操作,即第一个人反复拿两个奇数合成一个偶数,第二个人拿一个偶数一个奇数合成一个偶数。即每次消除三个奇数,然后答案减去一。可以 计算完成。

对于最后剩下的元素,再进行不超过一轮以后,只会剩下偶数,此时和不变。可以直接模拟最后的操作。

  • [n] 对于每个前缀都进行计算。时间复杂度
#include <fstream>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 1e5 + 5;
 
ifstream cin("makise.in");
ofstream cout("makise.out");
 
LL a[kMaxN], n, c0, c1, ans;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (LL i = 1; i <= n; i++) {
    cin >> a[i];
    c0 += a[i] % 2 == 0;
    c1 += a[i] % 2 == 1;
    LL x0 = c0, x1 = c1;
    LL sub = x1 / 3;
    x1 %= 3;
    ans += a[i];
    x0 += sub;
    if (x1 >= 2) {
      x1 -= 2;
      x0++;
    } else if (x0 >= 2) {
      x0--;
    } else if (x0 && x1) {
      x0--, x1--;
      sub++;
    }
    if (x0 && x1) {
      sub++;
    }
    cout << ans - sub << ' ';
  }
  return 0;
}

购物

贪心 优先队列 排序

赛时思路

考虑较多次地选择 最大(其次 )的的元素,枚举其它元素的选择次数(最多不超过 ),然后用优先队列每次选择其它元素当中可选的最大值。

注意

赛时思路是错误的。考虑这个 的样例

我们把 单独搞出去,枚举其它的选多少个。如果什么都不选就是 。当选择其它的一个时,程序将贪心地选择 。此时并不优。但是当选择其它的两个是,我们会选择 ,这直接导致了我们错过了最优解

猜想

这似乎需要通过反悔贪心实现,不过我的实现仍然无法成功地通过大样例。

  • [p] 好在只挂了 分。

对于这道题目,一共有两种思路。他们都是正确的。

正确思路一

既然我们不一定会选择 b_\max 较多次,于是我们就可以直接枚举选择哪个 ,那么就必定同样会选择 。对于其它元素 我们都不会再次选择了,所以我们把所有大于 给选了。可以通过排序后二分或双指针实现。时间复杂度

正确思路二

直接把一个物品拆成两个物品 从大到小加入优先队列当中( 还是 的信息仍要保留)。对于新的一个元素,如果是 直接选上即可;如果是 ,如果以前已经选过了其对应的 ,直接选上即可;否则若剩余步数允许就把它的 选上之后反复选 ,和答案求个 (注意这里不要 break 也不是实际选上了)。时间复杂度

  • [n] 我赛后改题实现的是正确思路二。
#include <algorithm>
#include <fstream>
#include <queue>
#include <utility>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 1e5 + 5;
 
ifstream cin("shop.in");
ofstream cout("shop.out");
 
LL f[kMaxN], t, n, m;
pair<LL, LL> a[kMaxN];
 
struct Node {
  LL id, val, tp;
 
  Node(LL id, LL val, LL tp) : id(id), val(val), tp(tp) {}
 
  friend bool operator<(const Node& a, const Node& b) {
    return a.val != b.val ? a.val < b.val : a.tp < b.tp;
  }
};
 
priority_queue<Node> q;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> t; t; t--) {
    cin >> n >> m;
    for (; q.size(); q.pop()) { }
    fill(f + 1, f + m + 1, 0);
    for (LL i = 1; i <= m; i++) {
      cin >> a[i].first >> a[i].second;
      q.emplace(i, a[i].first, 1);
      q.emplace(i, a[i].second, 2);
    }
    LL ans = 0, sum = 0;
    while (q.size()) {
      auto [id, val, tp] = q.top();
      q.pop();
      if (!n) {
        break;
      }
      if (tp == 1) {
        sum += val;
        ans = max(ans, sum);
        f[id] = 1;
        n--;
      } else {
        if (f[id]) {
          sum += val * n;
          ans = max(ans, sum);
          break;
        } else {
          if (n == 1) {
            continue;
          }
          ans = max(ans, sum + a[id].first + val * (n - 1));
        }
      }
    }
    cout << ans << '\n';
  }
  return 0;
}

划分

线段树 序列dp ST表 单调栈

思路

考虑 DP。由于和分段数量没有关系,所以设 表示若最后一段的最后一个元素为 ,所可以获得的最大价值。

转移如下:枚举最后一段实际的起点 ,那么前一段的终点就是 ,有 ,其中 其中 。不难发现可以通过倒叙枚举 维护最小值,时间复杂度优化为

优化

DP 状态设计已经足够简单了,如果优化转移复杂度呢?考虑维护全局最值,快速求出

考虑线段树维护 和上面一大坨。首先我们思考,对于前面的所有状态每个的后方都加入一个 ,谁的最小值会改变谁不会?可以找到最后一个 的就会改变,否则不会。

提示

可以用单调栈快速求,也可以用 ST 表维护 的最小值然后二分。

现在对于 后面的增量都会变成 。可以在线段树上直接做区间修改,拆成多段区间每段值都等于这一段 DP 最大值加上

现在 就可以通过查询线段树全局最小值得到。由于是查询全局不需要写懒标记下放。

考虑 往后面的转移,作为 DP 值插入到线段树 位置上即可。

时间复杂度 ,计算 DP 时间复杂度

#include <fstream>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 1e5 + 10, kLog = 20;
 
ifstream cin("divide.in");
ofstream cout("divide.out");
 
struct Node {
  LL dp, mx;
 
  Node(): dp(-1e18), mx(-1e18) { }
} tr[4 * kMaxN];
 
LL a[kMaxN], b[kMaxN], f[kMaxN][kLog], lg[kMaxN], dp[kMaxN], n;
 
LL query(LL l, LL r) {
  LL k = lg[r - l + 1];
  return min(f[l][k], f[r - (1 << k) + 1][k]);
}
 
Node merge(const Node &a, const Node &b) {
  Node res{};
  res.dp = max(a.dp, b.dp);
  res.mx = max(a.mx, b.mx);
  return res;
}
 
// 修改增量
void modifyAdd(LL p, LL l, LL r, LL s, LL t, LL val) {
  if (s <= l && r <= t) {
    tr[p].mx = tr[p].dp + val;
    return;
  }
  LL m = (l + r) / 2;
  if (s <= m) {
    modifyAdd(2 * p, l, m, s, t, val);
  }
  if (m < t) {
    modifyAdd(2 * p + 1, m + 1, r, s, t, val);
  }
  tr[p] = merge(tr[2 * p], tr[2 * p + 1]);
}
 
// 修改dp值
void modifyDp(LL p, LL l, LL r, LL x, LL val) {
  tr[p].dp = max(tr[p].dp, val);
  if (l == r) {
    return;
  }
  LL m = (l + r) / 2;
  if (x <= m) {
    modifyDp(2 * p, l, m, x, val);
  } else {
    modifyDp(2 * p + 1, m + 1, r, x, val);
  }
}
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (LL i = 1; i <= n; i++) {
    cin >> a[i];
    f[i][0] = a[i];
  }
  for (LL i = 1; i <= n; i++) {
    cin >> b[i];
  }
  for (LL j = 1; j < kLog; j++) {
    for (LL i = 1; i + (1 << j) - 1 <= n; i++) {
      f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
    }
  }
  for (LL i = 2; i <= n; i++) {
    lg[i] = lg[i / 2] + 1;
  }
  fill(begin(tr), end(tr), Node());
  modifyDp(1, 0, n, 0, dp[0] = 0);
  for (LL i = 1; i <= n; i++) {
    LL p = i;
    for (LL l = 1, r = i; l <= r; ) {
      LL m = (l + r) / 2;
      if (query(m, i) == a[i]) {
        p = m;
        r = m - 1;
      } else {
        l = m + 1;
      }
    }
    modifyAdd(1, 0, n, p - 1, i - 1, b[i]);
    dp[i] = tr[1].mx;
    modifyDp(1, 0, n, i, dp[i]);
  }
  cout << dp[n] << '\n';
  return 0;
}

树上计数

树链剖分 点分治 树形dp 启发式合并

我写的暴力。

思路

首先可以枚举三元组的中间点,使得三个点到枚举的点距离相同。这个点肯定不会在边上。

枚举后以 为根开始,跑出所有点的深度。那么我们求的以 为中心的三元组肯定深度相同,且在 的儿子的不同子树当中。这是一个组合数问题,可以线性解决。

时间复杂度 。成功获得 分。

正解是长链剖分+树形 DP ,当然淀粉质+树上启发式合并 也过了。