建筑升级

序列dp

首先看到 就很不能让人不想到 的 dp。dp 状态中,第一位表示长度为 的建筑前缀是必须的,那么第二维是什么呢?由于最终获得的额外分数和所有建筑的最小级别有关系,因此第二维就存前缀中所有建筑中最低的级别。

表示在前 个建筑中,等级最小为 ,最多能得多少分。那么对于新的一个建筑,假设我们选择让这个建筑升级到 级,那么有以下两种情况:

  • 如果 ,那么最低等级 不会改变;
  • 如果 ,那么最低等级 会变成

因此假设当前转移到 ,那么有两种方式,一种是从最小值比当前大的状态,但是当前选择的等级就是 的转移;另一种是最小值和当前一样,那么当前的等级就可以是任意比之前最小值大的而等级。

#include <iostream>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 1005;
 
LL a[kMaxN][kMaxN], dp[kMaxN][kMaxN], d[kMaxN], t, n, m, ans;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> t; t; t--) {
    cin >> n >> m;
    for (LL i = 1; i <= n; i++) {
      for (LL j = 1; j <= m; j++) {
        cin >> a[i][j];
        a[i][j] += a[i][j - 1];
      }
    }
    for (LL i = 0; i <= m; i++) {
      cin >> d[i];
      if (i >= 1) {
        d[i] += d[i - 1];
      }
    }
    fill(dp[0], dp[0] + m, -1e18);
    dp[0][m] = 0;
    for (LL i = 1; i <= n; i++) {
      for (LL j = m, pre = -1e18, mx = a[i][j]; j >= 0; j--) {
        mx = max(mx, a[i][j]);
        dp[i][j] = max(dp[i - 1][j] + mx, pre + a[i][j]);  // 最低等级不变或降低最低等级
        pre = max(pre, dp[i - 1][j]);
      }
    }
    ans = -1e18;
    for (LL i = 0; i <= m; i++) {
      ans = max(ans, dp[n][i] + d[i]);
    }
    cout << ans << '\n';
  }
  return 0;
}

快速蜗蜗变换

状压dp 枚举 集合 位运算

最开始我想到的是 01 trie,但是后面发现不能考虑到所有的情况。

最后还是写了个子集枚举,即对于每个 枚举 的二进制的子集,然后把 存入所有 的子集的值中间,并且分别取最大、最小值,然后倒序枚举 ,把之前的最大、最小 值取出来以四种情况中取最大值,然后再做一个后缀 ,累加即可。

正确性证明:如果 ,那么必然有 ,因为 有的每一个 都在 里面出现过,此时 ,然后我们再做一个后缀和就可以覆盖所有的 的情况。

时间复杂度高达 ,当 时大约是 左右,但是可以直接卡过。其实正解和这个区别不大,这个是枚举出所有的子集,正解是每次从已有的 中间掏去一个。

#include <iostream>
#include <algorithm>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 2e5 + 5, kMod = 998244353;
 
LL a[kMaxN], b[kMaxN], mxa[kMaxN], mxb[kMaxN], mna[kMaxN], mnb[kMaxN], t, n, ans, mx;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> t; t; t--) {
    cin >> n;
    for (LL i = 0; i < n; i++) {
      cin >> a[i];
    }
    for (LL i = 0; i < n; i++) {
      cin >> b[i];
      mna[i] = mnb[i] = 1e18;
      mxa[i] = mxb[i] = -1e18;
    }
    for (LL i = 0; i < n; i++) {
      for (LL x = i; x; x = (x - 1) & i) {
        mxa[x] = max(mxa[x], a[i]);
        mna[x] = min(mna[x], a[i]);
        mxb[x] = max(mxb[x], b[i]);
        mnb[x] = min(mnb[x], b[i]);
      }
      mxa[0] = max(mxa[0], a[i]);
      mna[0] = min(mna[0], a[i]);
      mxb[0] = max(mxb[0], b[i]);
      mnb[0] = min(mnb[0], b[i]);
    }
    ans = 0;
    mx = -1e18;
    for (LL i = n - 1; i >= 0; i--) {
      mx = max(mx, max(
                    max(mna[i] * mnb[i], mna[i] * mxb[i]),
                    max(mxa[i] * mnb[i], mxa[i] * mxb[i])));
      ans = ((ans + mx) % kMod + kMod) % kMod;
    }
    cout << ans << '\n';
  }
  return 0;
}

互不相同

贪心

我使用的是暴力。

对于子任务 3,可以采用迭代加深搜索,由于答案大概在 左右,因此最多也就迭代 次,单次瞎选一个区间,然后加一个特别大且特殊的值(我是 是当前深度,二进制保证不会重复),如果满足条件就可以退出了。这个搜索可以帮助我们找出子任务 1、2 的规律。

对于子任务 1,全部 都是 ,使用我们的搜索不难得到

对于子任务 2,我的式子猜错了,导致 wa 最后一个子测试点。

运货

平衡树

我的思路是错的。

我最开始想的是二分,二分出一个答案进行 判断,时间复杂度 ,但是答案根本没有单调性(如 6 3 2, 如果承重为 可以带 个,但是当承重为 时只能带 个),于是趋势。

总结

分数

下次写题目的时候不能看着好像可以二分就写二分,应该仔细判断单调性。然后更改做题侧重点,应该把重点放在第二题和第三题上而非第四题。