20250724 DP

First Come First Serve

有 n 个人来过,第 i 个人在 ai​ 时刻来在 bi​ 时刻走,每个人可以在来时或走时登记,问可能的登记顺序有多少种。

n⩽5×105,ai​,bi​ 互不相同,∀i<n,ai​<ai+1​,bi​<bi+1​。

考虑容斥,

答案是n^2但是会算重复,考虑如何不算重复?

有一个很强的限制,只要你选了b,那么a到b这段区间内必须有人选,否则不合法,发现这样就不会算重复了,那么考虑如何容斥。

选择 ai​,系数为 1。
选择 bi​,系数为 1。
若 (ai​,bi​) 间没有数被选择,选择 bi​,系数为 −1。

考虑 dp,设 dpi​ 表示考虑了 a/b1∼i​ 的选择,系数之积的和。

前两种选择对 dpi​ 的贡献显然为 dpi−1​。

对于第三种选择,我们对一个 [ai​,bi​],取最大的 p 满足 bp​<ai​,取最小的 q 满足 aq​>bi​,则 [p,q] 中的选择完全确定,对 dpq​ 的贡献为 dpp−1​。

#include <bits/stdc++.h>
using namespace std;

static const int MOD = 998244353;
using ll = long long;

// 快速读
inline int read() {
    int x = 0, c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while ('0' <= c && c <= '9') {
        x = x * 10 + (c - '0');
        c = getchar();
    }
    return x;
}

int main() {
    int n = read();
    vector<int> A(n+1), B(n+1);
    for (int i = 1; i <= n; i++) {
        A[i] = read();
        B[i] = read();
    }
    // 1) 计算 l[i] = max{ j | B[j] < A[i] }
    vector<int> l(n+1), r(n+1);
    for (int i = 1, j = 0; i <= n; i++) {
        while (j < n && B[j+1] < A[i]) j++;
        l[i] = j;
    }
    // 2) 计算 r[i] = max{ j | A[j] < B[i] }
    for (int i = 1, j = 0; i <= n; i++) {
        while (j < n && A[j+1] < B[i]) j++;
        r[i] = j;
    }
    // 3) 按 r 分组:所有区间 [l[i]+1 … r=i] 都会在 dp[r[i]] 时减去 dp[l[i]]
    vector<vector<int>> endsAt(n+1);
    for (int i = 1; i <= n; i++) {
        endsAt[r[i]].push_back(l[i]);
    }
    // 4) dp
    // dp[i] = 前 i 个人的方案数(已经做容斥去重)
    vector<ll> dp(n+1);
    dp[0] = 1;
    for (int i = 1; i <= n; i++) {
        // 每来一个人,先把所有现有方案 * 2(选 A 或 B)
        dp[i] = dp[i-1] * 2 % MOD;
        // 再减去「i 号选 B 且区间没人先选」的那些坏方案
        for (int li : endsAt[i]) {//for (l: v[i]) dp[i] -= dp[l] 就是把每一个“BiBi​ 且 [l+1,i−1][l+1,i−1] 内没人选” 的非法方案拿掉。
            dp[i] = (dp[i] - dp[li] + MOD) % MOD;
        }
    }
    cout << dp[n] << "\n";
    return 0;
}


也就是说,对于fi,需要容斥掉的方案有−2^−(Ri−Li+1)种

图片


绝顶之战

有一个长度为 mm 的一维空间,还有 nn 个物品,第 ii 个物品的长度为 aiai​。现在按照编号从小到大的顺序依次将物品放入空间中,对于第 ii 个物品:

如果当前空间中存在一段连续的长度至少为 aiai​ 的空余,则你必须将第 ii 个物品放入一段连续的长度为 aiai​ 的空余空间中。
否则,第 ii 个物品无法被放入,跳过它。

你需要输出:按照编号从小到大的顺序考虑完所有物品后,所有可能的空间占用长度,它的定义是所有被放入空间的物品的长度之和。


看起来需要先枚举所有选择的物品。

然后我们发现,空位的形成过程就是二叉树的建立过程,所以考虑对每一个目前已经2^n枚举的物品集合S,进行二叉树形态计数。

long long dfs(int S) {
	if (S == 0) return rmq[n];
	if (f[S] != 0) return f[S];
	
	int i = __builtin_ctz(S);
	int SS = S ^ (1 << i);
	for (int T = SS;; T = (T - 1) & SS) {
		long long val = dfs(T) + dfs(SS ^ T) + a[i];
		if (val > f[S]) f[S] = val;
		if (T == 0) break;
	}
	
	long long x = rmq[i];
	if (x < f[S]) f[S] = x;
	if (sum[S] >= x) {
		f[S] = -INF;
	}
	return f[S];
}

其中rmq是当前集合中最小可能,超过了就一定不合法,这是一个剪枝。


Negative Cost

一头怪兽挡在你的面前,它有 H 的血量。

你有 n 种技能,每个技能都可以以任意合法顺序无限次使用。

你有一个魔力值,初始为 0 。

第 i 种技能会消耗 Ci​ 的魔力值,并对怪兽造成 Di​ 的伤害。你要时刻保证魔力值非负。

特别的, Ci​ 可能为负数,此时该技能会增加 −Ci​ 的魔力值。

当怪物血量不为正数时,怪物就死了。

问:最少发动多少次技能,能杀死这只怪物?


任何合法操作序列都可以拆分成若干个C总和小于2L的序列,且满足长度和造成的伤害相等。

并且这些序列还可以继续拆分,使得长度也小于2L,这样里面就是一个裸的背包。

然后一共有多少种拆法呢?不难发现只有O(C^2)中(其实我去问DK了)

那么现在问题就变成了,我们有 0∼2L 这些连招,问我们如何释放连招,才能使得杀死 monster 的耗费步数最少。

然后再用这些小的拆法用背包算,由于证明,非性价比最高的连招至多使用 2L 次。

所以剩下的选最优性价比连招(即为D/L最优的)

记二维数组 f[i][j]:长度为 i、魔力为 j(从 0 到 \(2L\))时的最大伤害。
然后DP
每个 D[i] 记录长度为 i 的极小优秀操作序列最大伤害。

并找到单位长度性价比最高的连招 key

构造 g[i] 表示长度为 i 时构成的最大伤害。

枚举使用至多 \(2L\) 次连招,总长度 \(\le (2L)^2\)

之后用性价比最高的连招补全剩余生命值。

#include <bits/stdc++.h>
using namespace std;

constexpr int maxn = 310;  // 最大技能数
constexpr int maxl = maxn * 2;  // 最大魔力值范围,即 2L
#define int long long 
constexpr int INF = (int)1e18;  // 设定最大值为无穷大,方便后续最小化

int n, H, L = 300;  // n: 技能数, H: 怪兽血量, L: 魔力范围的最大值
struct A {
    int c, d;  // c: 魔力消耗, d: 伤害
} a[maxn];  // 存储所有技能的信息

int f[maxl + 1][maxl + 1];  // 动态规划数组,f[i][j] 表示长度为 i、魔力为 j 的极小优秀操作序列能打出的最大伤害
int D[maxl + 1];  // D[i] 存储长度为 i 的极小优秀操作序列所能打出的最大伤害
int g[(maxl * maxl) + 1];  // g[i] 表示长度为 i 的合法操作序列造成的最大伤害

signed main() {
    #ifdef OFLINE
    freopen("in.in", "r", stdin);  // 本地调试时重定向输入
    freopen("out.ouw", "w", stdout);  // 本地调试时重定向输出
    #endif
    
    ios::sync_with_stdio(false);  // 关闭同步,提高 I/O 性能
    cin.tie(nullptr);  // 关闭 cin 与 cout 之间的同步

    cin >> n >> H;  // 输入技能数和怪兽血量
    for (int i = 1; i <= n; i++) {
        int c, d;
        cin >> c >> d;
        a[i].c = -c;  // 反转魔力消耗:负数表示增加魔力
        a[i].d = d;  // 储存伤害
    }
    
    // 初始化动态规划数组 f 为负无穷,表示不可达状态
    memset(f, 0x80, sizeof(f));  // 初始化为 -INF
    f[0][0] = 0;  // 初始状态:0 次操作,魔力为 0,造成的伤害为 0

    // 动态规划:计算所有合法操作序列的最大伤害
    for (int i = 1; i <= 2 * L; i++) {  // 遍历操作序列的长度
        for (int j = 0; j <= 2 * L; j++) {  // 遍历操作序列的魔力
            for (int q = 1; q <= n; q++) {  // 遍历所有技能
                int prev = j - a[q].c;  // 上一状态的魔力
                if (prev >= 0 && prev <= 2 * L && f[i - 1][prev] >= 0) {  // 确保上一状态合法
                    f[i][j] = max(f[i][j], f[i - 1][prev] + a[q].d);  // 更新当前状态的最大伤害
                }
            }
        }
    }

    // 计算长度为 i 的极小优秀操作序列能造成的最大伤害 D[i]
    for (int i = 1; i <= 2 * L; i++) {
        for (int j = 0; j <= 2 * L; j++) {
            D[i] = max(D[i], f[i][j]);  // 对每个长度 i,取最大伤害
        }
    }

    // 计算性价比最高的连招的长度和对应的伤害
    int maxvalpos = 1;
    double maxval = (double)D[1] / 1;  // 初始化最大性价比(伤害 / 连招长度)
    for (int i = 2; i <= 2 * L; i++) {  // 从 2 到 2L 遍历,计算性价比
        double avg = (double)D[i] / i;  // 当前长度 i 的性价比
        if (avg > maxval) {  // 更新最大性价比
            maxval = avg;
            maxvalpos = i;  // 保存性价比最高的连招长度
        }
    }

    // 初始化组合背包 dp 数组,计算所有可能的伤害
    memset(g, 0x80, sizeof(g));  // 初始化为负无穷,表示不可达
    g[0] = 0;  // 初始状态:0 次操作,造成的伤害为 0

    // 计算通过组合极小优秀操作序列,最多能造成多少伤害
    for (int i = 1; i <= 2 * L * 2 * L; i++) {  // 遍历所有可能的操作序列长度
        for (int j = 1; j <= 2 * L && j <= i; j++) {  // 遍历每个连招长度
            g[i] = max(g[i], g[i - j] + D[j]);  // 更新当前长度 i 的最大伤害
        }
    }

    // 计算最终最少的操作次数
    int ans = INF;  // 初始化答案为无穷大
    for (int i = 0; i <= 2 * L * 2 * L; i++) {  // 遍历所有可能的操作序列长度
        int rest = max(0LL, H - g[i]);  // 剩余的血量
        int t = (rest + D[maxvalpos] - 1) / D[maxvalpos];  // 需要多少次最优连招来补充剩余血量
        ans = min(ans, i + t * maxvalpos);  // 最小化操作次数
    }

    cout << ans << "\n";  // 输出结果:最小的操作次数
    return 0;
}


Permutation Counting 2


为了这道题,我电脑蓝屏了。


图片

图片

图片

图片
图片

图片
图片

posted @ 2025-07-24 21:35  Dreamers_Seve  阅读(6)  评论(0)    收藏  举报