SCOI2024day1t3重整山河 题解

我去我好像做不太懂这道题,有无大手子教教/ll


题意

\(n\) 只怪兽,每只怪兽有两个属性 \((a_i,b_i)\)。其中 \(a_i\) 表示生命值,\(b_i\) 表示爆炸伤害。每次你选择一只怪兽攻击;一只怪兽死亡后会对所有怪兽造成 \(b_i\) 点伤害。怪兽被攻击/生命值降为 \(0\) 则立刻死亡。需要求出最小攻击次数。

但是现在只给定 \(a_i\),只知道 \(b_i \in [l_i,r_i]\)。你需要对所有 \(b\) 序列,求出最小攻击次数的和。

\(M\) 取模。

\(1 \le n \le 20,1 \le l_i \le r_i \le 15,1 \le a_i \le 300,2 \le M \le 10^9+7\)

解法

1

考虑 \(b\) 序列固定,如何算答案。

注意到你可以化身白金之心,来一手神之时停:钦定一个攻击集合 \(S\),攻击完一个人让它先别炸,等全部攻击完最后一起炸。

然后我们按 \(a_i\) 升序排序,则最终死亡怪兽必然是一个前缀。假设我们从前往后依次考虑每个怪兽是否能够死亡,怪兽 \(i\) 能死亡等价于 \(i \in S \lor \sum_{j=1}^{i-1}b_j+\sum_{t=i+1}^{n}b_t[t \in S] \ge a_i\)

时间复杂度 \(O(2^nn)\)

2

下文上式统一指代 \(\sum_{j=1}^{i-1}b_j+\sum_{t=i+1}^{n}b_t[t \in S] \ge a_i\)

称一个元素【可以自然死亡】等价于【满足上式】。

但是这太区了。经验告诉我们,想要计数,起码解决子问题的复杂度要是多项式的。

注意到上式中我们只关心后缀 \(S\)\(b\) 的和。所以不妨从后往前依次考虑每个元素是否属于 \(S\):如果它满足上式,则你不攻击它它也能自然死亡,所以我们先不攻击它;否则出现了两个选择:攻击它,或攻击后缀尚未被攻击的一些怪兽使得它满足上式子。

\(T = \{b_j|j \text{ haven't been attacked so far}\}\),记 \(mx\) 表示 \(T\) 中的最大值。我们进行一个分讨:若 \(b_i \ge mx\),攻击 \(a_i\) 既最大化 \(now\) 又最小化攻击次数,所以是最优的;否则 \(b_i \lt mx\),考虑 \(mx\) 所属的怪兽 \(j\),其生命值和爆炸伤害都偏序怪兽 \(i\),我们肯定优先攻击怪兽 \(j\)。并且可以证明,攻击完怪兽 \(j\)\(i\) 就一定满足上式,不会再取 \(T\) 中的第二个元素攻击。(感性理解就是攻击 \(j\) 不劣于攻击 \(i\),而攻击 \(i\) 的新增攻击次数是 \(1\);理性证明就是考虑 \(i\) 右边第一个未被攻击的怪兽(一定存在),考虑二者式子左边的差,发现至多为 \(b_i\),而我们现在新增 \(b_j \gt b_i\),且 \(a_i\) 还变小了,必然满足限制。)

所以我们得到做法:维护 \(s\) 表示 \(b\) 的前缀和。从后往前扫,用大根堆维护集合 \(T\),记 \(now\) 表示已决策元素的 \(b_i\) 之和。每次先将 \(b_i\) 加入大根堆,然后如果 \(s_{i-1}+now \ge a_i\) 不成立,则决策堆顶元素并弹出。时间复杂度 \(O(n \log n)\)

std::priority_queue<int> q;
FOR(i,1,n)(bs[i]=bs[i-1])+=b[i];
int ct = 0,now = 0;
REP(i,n,1){
	q.push(b[i]);
	if(bs[i-1]+now<a[i].a)now+=q.top(),q.pop(),adx(ct,1);
}

3

考虑 \(dp\)。我们无法用多项式的信息记下大根堆。一个比较自然的想法是钦定一个 \(mi\) 表示 \(\ge mi\) 的元素都钦定在未来某个时刻出队。但是写了之后发现不是重就是漏不然就又重又漏。

考虑本质一点的做法。

从后往前依次考虑每个 \(b_i\) 是否 \(\in S\)。记 \(H = \{b_j|j \text{ 可以自然死亡,但是在考虑未来某个元素时被从大根堆里弹出,因此预先钦定} \in S\}\)\(G = \{b_j|j \text{ 可以自然死亡,并且不存在一个未来元素需要将其从大根堆里弹出,因此钦定从头到尾} \notin S\}\)

如果 \(b_i\) 不能自然死亡,并且后缀中 \(\ge b_i\) 都被击杀/钦定以后击杀了,那么只能当场击杀 \(b_i\);否则 \(b_i\) 能自然死亡,我们需要考虑 \(b_i \in H\)\(b_i \in G\) 的情况。注意到保证 \(H\) 的最小值 \(\ge\) \(G\) 的最大值,我们似乎可以任意决策。

但是这里有个小问题:如果有多个 \(b_j\) 相等,且其中一部分 \(\in S\) 另一部分 \(\in G\),如果直接套用上述条件,在钦定集合大小后,仍然可能出现梦到哪个集合加哪个集合的情况,显然会算重。因此,对于 \(H\) 最小值等于 \(G\) 最大值等于 \(v\) 的情况,需要选择先出现的一段 \(v \in H\),剩下后出现的 \(v \in T\)

考虑什么时候结算钦定元素。即对于能自然死亡的 \(b_i\),我们选择将其加入 \(G\),并且在 \(H\) 的最小值 \(\ge b_i\) 的前提下,去掉 \(H\) 中的最小值就不能自然死亡时。这就是之前贪心判定时 \(mx \ge b_i\) 的情况。正是为了此时,才有了 \(H\)。这时我们就可以结算并清空 \(H\),准备重新钦定。

那么到这里就可以 \(dp\) 了。

维护七元组 \((i,s,bs,mx,mi,a,b)\) 表示考虑到第 \(i\) 位,钦定 \(s_{i-1} = s\),后缀 \([i,n]\) 中钦定以后击杀/当场击杀元素 \(\sum b_i = bs\),自由选择的元素中(即被考虑到时满足式子)选择不被击杀的元素的最大值为 \(mx\),被钦定以后击杀的元素最小值为 \(mi\),攻击次数总和为 \(a\),方案数为 \(b\)

转移看代码。

#include <cstdio>
#include <cstdlib>
#include <cctype>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#define FOR(i,a,b) for(int i = (a);i <= (b);++i)
#define REP(i,a,b) for(int i = (a);i >= (b);--i)
static char stkk[200];
template<typename T>inline void output(T x){
    if(!x)return putchar('0'),void();
    if(x<0)x = ~x+1,putchar('-');
    int top = 0;
    for(;x;stkk[++top]=x%10^48,x/=10);
    for(;top;putchar(stkk[top--]));
}
template<typename T>inline void readx(T &x){
    x = 0;int y = 1;char c = getchar();
    for(;c<48||c>58;c = getchar())if(c=='-')y = -1;
    for(;c>=48&&c<=58;c = getchar())x = (x<<1)+(x<<3)+(c^48);
    x *= y;
}
const int N = 22,KM = 301,G = 17,nf = 1e9+10;
static int n,m,M;
inline void adx(int &x,int y){
    if((x+=y)>=M)x-=M;
}
struct node{
    int a,b;//a = sum ct,b = sum num
    node& operator+=(const node& A){
        adx(a,A.a),adx(b,A.b);
        return *this;
    }
    node& operator*=(const node& A){
        adx(a,A.a),adx(a,A.b),adx(b,A.b);
        return *this;
    }
    bool noten()const{
        return a||b;
    }
    void clear(){
        a = b = 0;
    }
}f[2][20*15+1][20*15+1][17][17];
struct node2{
    int a,l,r;
    bool operator<(const node2& A)const{
        return a<A.a;
    }
}a[N];
static int sl[N],sr[N];
inline void ckmax(int &x,int y){
    x<y&&(x=y);
}
signed main(){
    readx(n),readx(m),readx(M);
    FOR(i,1,n)readx(a[i].a),readx(a[i].l),readx(a[i].r);
    std::sort(a+1,a+1+n);
    FOR(i,1,n)(sl[i]=sl[i-1])+=a[i].l,(sr[i]=sr[i-1])+=a[i].r;

    // printf("a::");FOR(i,1,n)printf("%d ",a[i].a);putchar(10);
    // printf("l::");FOR(i,1,n)printf("%d ",a[i].l);putchar(10);
    // printf("r::");FOR(i,1,n)printf("%d ",a[i].r);putchar(10);

    int now = 1,pre = 0;
    FOR(s,sl[n],sr[n])f[1][s][0][0][m+1] = (node){0,1};
    REP(i,n,1){
        // printf("  i:%d\n",i);
        std::swap(now,pre);
        FOR(s,sl[i],sr[i])FOR(bs,0,sr[n])FOR(mx,0,m)FOR(mi,0,m+1){
            const node &nd = f[pre][s][bs][mx][mi];
            if(nd.noten()){
                // printf("    s:%d bs:%d mx:%d mi:%d nd.a:%d nd.b:%d\n",s,bs,mx,mi,nd.a,nd.b);
                FOR(k,a[i].l,a[i].r){
                    if(k>s)break;
                    if(s-k+bs>=a[i].a){
                        int tmi = s-k+bs-mi<a[i].a?m+1:mi;
                        if(k<=mi)f[now][s-k][bs][std::max(mx,k)][tmi]+=nd;
                        if(k>mx)f[now][s-k][bs+k][mx][std::min(k,mi)]*=nd;
                    }
                    else{
                        if(k>mx)f[now][s-k][bs+k][mx][mi]*=nd;
                    }
                }
            }
        }
        FOR(s,0,sr[n])FOR(bs,0,sr[n])FOR(mx,0,m)FOR(mi,0,m+1)
            f[pre][s][bs][mx][mi].clear();
    }
    node ans = (node){0,0};
    FOR(bs,0,sr[n])FOR(mx,0,m)
        ans+=f[now][0][bs][mx][m+1];
    output(ans.a),putchar(10);
    return 0;
}

posted @ 2026-08-04 18:03  rabbit_mygo  阅读(23)  评论(1)    收藏  举报