「联合省选 2020 B 卷」做题记录

「联合省选 2020 B 卷」做题记录

题目链接

卡牌游戏

思维题,贪心

考虑刻画一次操作带来的收益。记 si=j=1iaj ,即 a 的前缀和。可以发现,如果在某个时刻选择第 ii>1)张卡牌进行操作,得到的收益是恒定的 si,和之前如何操作无关。所以选择所有满足 si>0 的位置 i(不包括第一个位置)进行操作就是最优的。

幸运数字

枚举,离散化

做这道题时,一开始我陷入了一个错误的方向,就是直接考虑如何最大化收益。假设没有奖励条件的限制,只考虑选择一堆 w 异或起来,使得异或和最大,这可以用线性基来做。但我们很难把奖励条件的限制加入进来。

这时候就要考虑换一个方向了!不妨看看第一个部分分:当值域很小时,可以枚举所有数字(显然,枚举 [min{L,A,B}1,max{R,A,B}+1] 之内的数字就行),对每个数字在 O(n) 时间内计算选择它时的奖励额度。进一步,我们实际上无需对每个数字都花费 O(n) 的时间计算选择它时的奖励额度:使用递推的思想,从小到大枚举数字,动态地更新奖励额度,可以把总时间复杂度降到 O(V)。(其中 V 代表 L,R,A,B 的值域大小)

想到这步以后正解也近在咫尺了:显然有很多数字的奖励额度是相同的,所以并不需要枚举 O(V) 个数字。离散化以后只有 O(n) 个有用的数字。不过,只离散化 L,R,A,B 这些数字是错误的,容易找出反例。正确的做法是,除了把上述数离散化,还要把形如 x1x+1 的数也离散化。除此之外为了处理答案为 0 的情况,还要把 0 也离散化。

AC 记录

冰火战士

数据结构

写了三天,总共交了 16 发,战绩可查

先观察一些性质。

首先,由于只有当一方战士的能量值全部耗尽时战斗才会结束,所以战士出战的顺序其实无关紧要。又因为双方消耗的能量值相等,所以消耗的总能量值是能量值较小一方的二倍。设在温度 k 下冰系战士出战的能量值总和为 f(k),火系战士为 g(k),则消耗的总能量值为 F(k)=2min(f(k),g(k))。(需要满足 f(k)>0g(k)>0。)

把双方战士按自身温度从低到高排序,可以发现冰系战士中能够出战的形成一段前缀,而火系战士中能够出战的构成一段后缀。那么 f(k) 单调增而 g(k) 单调减。因此存在某个温度 p,使得 kpf(k)<g(k)k>pf(k)g(k)。在第一种情况时有 min(f(k),g(k))=f(k)f(k) 在此条件下最大,第二种情况时有 min(f(k),g(k))=g(k)g(k) 在此条件下最大,因此分这两种情况讨论即可。

将询问离线,把温度离散化并据此建立树状数组,树状数组上某个位置 x 的权值代表温度为 x 的冰/火系战士的能量之和。(一开始看到有加入/删除的操作,我只想到了平衡树,而没有想到可以离线后使用树状数组。如果用平衡树,肯定会由于常数太大而过不了,并且代码也远远比树状数组难写。)设不同的温度的个数为 N,则我们能在 O(logN) 的时间内查询一段温度区间内冰/火系战士的能量总和,因而也就能查询某温度下出战的冰/火系的能量总和。

现在通过二分找出分界点 p。由于温度是离散的,所以这个地方的细节比较多,需要想清楚再写。具体而言,第一次二分先找出最后满足 f(p)<g(p)p

int lo = 1, hi = Q, p = -1;
while(lo <= hi) {
    int mid = (lo + hi) >> 1;
    int pre = tr[0].query(1, mid), suf = tr[1].query(mid, Q);
    if(pre < suf) p = mid, lo = mid + 1;
    else hi = mid - 1;
}

然后,设 q 表示满足 g(q)f(q)(注意这里可以取等,否则考虑的情况不全)的第一个位置。但 q 并不一定是第二种情况的答案,因为可能不满足温度最大。所以要求的实际上是 q,满足 q 是最后一个使得 g(q)=g(q) 的位置。

要求出 q,显然可以直接再二分一次。但另一种更好的方法是复用之前求出的 p,因为 p 的下一个位置 p+1 肯定满足 f(p+1)g(p+1),所以直接令 qp+1 即可。但这里有个问题,就是不一定存在满足 f(p)<g(p)f(p)0p,这时候还能直接令 qp+1 吗?实际上是可以的,分两种情况讨论:

  1. 不存在 k 使得 f(k)g(k) 同时非 0

    (注意,虽然图中两条曲线看起来有交,并且在交点处两者同时非 0,但由于函数是离散的,所以它们实际上不存在交点)

    这种情况下,无论什么温度,双方都不能开战。由于此时一定有 g(p+1)=0,所以直接判掉这种情况即可。

  2. 存在 k 使得 f(k)g(k) 同时非 0,但不存在 p 满足 f(p)<g(p)f(p)0

    这种情况下一定有 g(p+1)>0,所以可以令 qp+1

综上所述,我们就在没有再次二分的情况下得到了 q。记 x=g(q),再二分一次得到最大的 q 使得 g(q)=g(q) 即可。

在树状数组上查询的时间复杂度为 O(logN),二分的时间复杂度也为 O(logN),总时间复杂度为 O(Qlog2N)在常数较小的时候可以通过。(根据试验,如果用二分找到 q,则每次询问要二分三次,此时只能得到 60 pts,但如果 q 不是用二分找的,则每次询问只用二分两次,可以得到 100 pts。)

要继续优化,可以使用树状数组二分的技巧。实际上与其说是二分,它更像是倍增。在树状数组中,点 x 维护是长为 lowbit(x) 区间 [xlowbit(x)+1,x]。以二分 p 为例,初始时 p=0,从 log2N0 倒序枚举 i,每次尝试令 p 加上 2i,判断扩展以后 f(p+2i)<g(p+2i) 是否成立,如果成立就扩展,否则撤回。这里的要点在于,用 pre记录当前的 f(p),扩展时,不用在树状数组上查询,而是直接令 prepre+c(p+2i) 即可。(其中 c 是树状数组中的数组。)这是因为,由于我们倒序枚举 i,所以一定有 lowbit(p+2i)=2i,因此 c(p+2i) 维护的就是 [p+1,p+2i] 的信息。这就就成功把单次二分的时间复杂度降到了 O(logN),总时间复杂度降为 O(QlogN)

参考代码(部分变量名称可能不同):

int pre = 0, suf = tr[1].query(1, V), p = 0;
for(int i = __lg(V), lst = 0; i >= 0; i--) {
    p += 1 << i;
    if(p <= V && pre + tr[0][p] < suf - tr[1][p] - lst + sum[p]) {
        pre += tr[0][p], suf = suf - tr[1][p] - lst + sum[p];
        lst = sum[p];
    } else {
        p -= 1 << i;
    }
}

AC 记录

信号传递

状压 dp

约定:记 p 代表信号站构成的排列;U=[1,n]Z

本题的关键在于发现花费是可以拆的。也就是说。假设序列 S 中存在相邻的数对 (x,y),那么产生如下的费用:

cost(x,y)={pypx, if pypxkpx+kpy, Otherwise

换一种表述方法,把费用都摊到 x 上:每有一个 (x,y) 数对,则在 x 处产生 px(若 pypx)或 kpx(若 py<px)的费用;每有一个 (y,x) 数对,则在 x 处产生 px(若 pypx)或 kpx(若 py>px)的费用。那么总费用就是所有站点的费用之和。

上述观察是进一步解决问题的基础。

看到 m 很小,自然想到状压。设 f(s) 表示把集合 ssU)中的信号站放到前 |s| 个位置时,它们产生的最小花费。(正因为我们把费用拆开了,我们才能在排列未完全确定的情况下,能够计算出一部分信号站的贡献)转移时,枚举 s 之外的一个信号站 i,转移到 f(s{i}) 这个状态。实际上这相当于要求:已知前 |s| 个位置的信号站集合为 s,在第 (|s|+1)(下面称为 p)个位置放第 i 个信号站,要能求出 i 的花费。观察花费的式子,我们发现这是可以求的,因为一个信号站的花费只与在它之前和之后的信号站集合有关,而顺序是无所谓的。

c(x,y) 表示 S 序列中相邻数对 (x,y) 的个数,则有如下转移方程:

f(s{i})f(s)+pjS(c(i,j)k+c(j,i))+jSji(c(i,j)+c(j,i)k)

状态数为 O(2m),转移时间复杂度为 O(m2),总时间复杂度为 O(n+2mm2)

考虑优化。对于给定的 si,则 p 的系数为 jS(c(i,j)k+c(j,i))+jSji(c(i,j)+c(j,i)k),记为 g(s,i)。转移的瓶颈主要在于求出这个系数,如何优化?实际上它可以递推求出:设 js 内的任意一个元素,那么如果已知 g(s{j},i),则 g(s,i) 容易在 O(1) 时间内求出:重新计算 (i,j)(j,i) 数对的贡献即可。为了方便,不妨选择 s 中最小的元素作为 j,代码实现上有 j=lowbit(s)。(这里用的是填表法)这样我们就把时间复杂度降为 O(n+2mm),但空间复杂度升为 O(2mm)

现在时间复杂度已经足够低,但计算发现 2mmint 的空间占用约为 736 MB,超过了空间限制,所以还要优化空间占用。由于无论是 g 还是 f 的转移,都是将一个大小为 |s| 的集合转移到大小为 |s|+1 的集合,所以如果按集合的大小从小到大转移,同一时间最多只有 ((2311)+(2312)) 个有用的状态,这是开得下的。

必须同时转移 fg,这样才能保证 fg 转移的进度是一样的。值得一提的是,f 用刷表法转移比较方便,而上文中 g 使用了填表法转移(也就是 g(s,i)g(slowbit(s),i) 转移而来),如果同时转移,必须把二者统一起来。不妨考虑把 g 的转移改成刷表法。也就是说,对于集合 s,它转移到状态 t,需要满足 tlowbit(t)=s。(实质上我们是在要求每个状态只能从之前的 1 个状态转移而来,这样才能保证正确的时间复杂度。实际上把 g 的一个的状态转移多次也不会导致答案错误,但会增加时间开销)可以这么写代码:

for(int i = 1; i <= m; i++) {
    if(get(s, i)) break; // 如果s的某一位为1,则退出循环
    int t = s | (1 << (i - 1));
    // 转移到g(t, *)
}

代码实现上,可以使用滚动数组或者队列。使用队列转移的过程类似 bfs。具体而言,把长为 m 的数组 g(s,) 记为一个状态,放到队列中转移即可。

Code
st = (1 << m);
State s0(0);
for(int i = 1; i <= m; i++) {
    for(int j = 1; j <= m; j++) {
        if(i == j) continue;
        s0.g[i] += -cnt[i][j] + K * cnt[j][i];
    }
}
f.resize(st, INF), f[0] = 0;
while(!que.empty()) {
    auto [s, g] = que.front();
    que.pop();
    int pos = __builtin_popcount(s) + 1;
    // update f
    for(int t = (st - 1) ^ s; t; t -= lb(t)) {
        // s -> s cup {i}
        int i = __builtin_ctz(t) + 1, news = s + lb(t);
        chmin(f[news], f[s] + pos * g[i]);
    }
    // update g
    for(int i = 1; i <= m; i++) {
        if(get(s, i)) break;
        // make sure that s = t - lb(t)
        int t = s | (1 << (i - 1));
        State nxt(t);
        for(int _t = (st - 1) ^ t; _t; _t -= lb(_t)) {
            int j = __builtin_ctz(_t) + 1;
            nxt.g[j] = g[j] - (-cnt[j][i] + K * cnt[i][j]) + (K * cnt[j][i] + cnt[i][j]);
        }
        que.push(nxt);
    }
}

AC 记录

总结:如果题目给出的式子不好直接入手,要想到拆贡献!除此之外本题的代码实现用到了许多二进制的技巧,值得学习。

消息传递

点分治

点分治板子题。

设当前的分治中心为 u,先计算出 T(u) 中所有点到 u 的距离,并用桶记录每种距离的个数。然后依次处理每棵子树。具体而言,对于子树 v,处理它时先在桶中减去它的贡献,这时桶内记录的就是 T(u)T(v) 的距离。然后枚举 xT(v) 内的询问,并累加答案即可。处理完 T(v) 后记得把它的贡献加回来。

由于每次分治时,每个节点和询问都只会被访问 O(1) 次,而如果每次选择重心作为分治中心,分治的次数为 O(logn),所以总时间复杂度为 O((n+m)logn)

丁香之路

前面的路,以后再来探索吧!

总结

gamelucky 都比较简单,冷静思考就能想出来。

icefire 是比较传统的数据结构题。一开始问题的转化是很容易的,转化之后很明显要用数据结构优化。但我还是 ds 题做的太少,一开始居然以为要用平衡树。这道题的实现还比较考验代码细节,我写了好几天才调出来。总而言之,对于这种没什么思维难度的 ds 题,还是要尽量拿下的。

transfer 是有一定思维难度的 dp。根据数据范围很容易猜到这是状压 dp,但我 dp 也很菜,没有想出什么可用的状态设计。实际上根据位置设计状态应该还是比较常见的,有了这个方向以后自然回去尝试拆贡献,拆完贡献以后直接 dp 就有 60 分,后面的优化也都比较常规。

message 纯板子。

lilac 不会。感觉比较思维。

posted @   DengStar  阅读(17)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示