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
为了这道题,我电脑蓝屏了。