P3619 魔法
题目大意:
给定 \(n\) 对元素与数字 \(T\),可以按任意顺序遍历所有元素,对于当前元素 \(i\),若 \(T>t_i\),则 \(T\gets T+v_i\),否则遍历失败。求问是否存在一种顺序可以遍历 \(n\) 对元素。
其中 \(1\le n,T,t_i\le 10^5,-10^5\le v_i\le 10^5\)。
将元素对分成两部分:\(v_i\ge 0\) 与 \(v_i<0\)。
显然,先处理 \(v_i\ge 0\) 再处理 \(v_i<0\) 是最优的。
对于 \(v_i \ge 0\):感性理解一下,我们与其去赌 \(t_i\) 大的,不如先老老实实的拿稳小的,因为 \(T\) 一直是增加的,所以 \(t_i<t_j\) 的时候是最优的。
对于 \(v_i < 0\),进行相邻点分析。
有两对相邻元素 \((t_i,v_i)\) 与 \((t_j,v_j)\),设当前时间为 \(T\),若 \(i\) 前 \(j\) 后,则:
\\\begin{cases}T\>t_i\\\\T+v_i\>t_j\\end{cases} \\
进一步,可得:
\T\>\\max(t_i,t_j-v_i) \\
同理,若 \(i\) 后 \(j\) 前,则:
\T\>\\max(t_j,t_i-v_j) \\
如果我们想让 \(T\) 的限制更宽松一点,即 \(i\) 在前优于 \(j\) 在前就要求:
\\\max(t_i,t_j-v_i)\<\\max(t_j,t_i-v_j) \\
注意到 \(v<0\),所以 \(t<t-v\),因此比较的关键在于 \(t_j-v_i<t_i-v_j\),即:
\t_i+v_i\>t_j+v_j \\
按照这个排序就好了。
cpp
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 10;
int n, T;
struct node {
ll t, w, p;
ll prz;
} a[N];
bool cmp(node x, node y) {
if (x.prz != y.prz) return x.prz > y.prz;
if (x.prz == 1) return x.t < y.t;
if (x.prz == -1) return x.t + x.w > y.t + y.w;
}
void solve() {
cin >> n >> T;
for (int i = 1; i <= n; i++) {
cin >> a[i].t >> a[i].w;
if (a[i].w >= 0) a[i].prz = 1;
if (a[i].w < 0) a[i].prz = -1;
a[i].p = max(a[i].t, -a[i].w);
}
sort(a + 1, a + n + 1, cmp);
ll now = T, is = 1;
for (int i = 1; i <= n; i++) {
if (now <= 0 || now <= a[i].t) {
cout << "-1s\n";
return ;
}
now += a[i].w;
}
if (now <= 0) cout << "-1s\n";
else cout << "+1s\n";
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int Z; cin >> Z;
while (Z--) solve();
return 0;
}
P3214 HNOI2011 卡农
题目大意 :我们要在 \(n\) 个音阶的所有非空子集(即片段)中挑选 \(m\) 个不同的子集,使得每个音阶在所有挑出的片段中出现的总次数为偶数。两个选取方案如果在调整片段的顺序后相同,则被视作同种音乐。
我们可以将每种片段看作是一个长度为 \(n\) 的二进制数,由于不能是空集,每个二进制数的值都在 \(1\) 到 \(2^n - 1\) 之间。
题目等价于:在 \(1\) 到 \(2^n - 1\) 中选出 \(m\) 个互不相同 的数,且它们的异或和为 \(0\)(因为每种音阶出现的次数都为偶数,即在同一位上的 \(1\) 个数为偶数,异或必然为 \(0\))。最后由于不考虑选取顺序,我们需要将得到的方案数除以 \(m!\)。
计算方案数肯定要 DP 呀。
- 状态 :定义 \(f_i\) 表示有序地挑选 \(i\) 个互不相同的非 \(0\) 数,并且它们的异或和为 \(0\) 的方案数。
- 状态转移方程:
设 \(P_{i-1}\) 为任选 \(i-1\) 个互不相同的非 \(0\) 数的方案数(不管异或和):
\P_{i-1} = (2\^n - 1)(2\^n - 2)\\dots(2\^n - i + 1) \\
我们只需要从中减去非法的方案就好了。
第一种情况:若第 \(i\) 个数为 \(0\),则要求前 \(i-1\) 个元素异或和为 \(0\) 的方案数,即 \(f_{i-1}\)。
第二种情况:
第 \(i\) 个数与前 \(i-1\) 个数中的某个数相等,假设与第 \(k\) 个数相等。这意味着除了第 \(k\) 个数以外的其余 \(i-2\) 个数异或和为 \(0\)。
我们可以把它们看作是一个长度为 \(i-2\) 且异或和为 \(0\) 的合法方案。对于每种这样的合法方案,有 \(i-1\) 个位置可以安插第 \(k\) 个数。第 \(k\) 个数的取值可以是除去这 \(i-2\) 个数以及 \(0\) 之外的任意一个数,共有 \((2^n - 1) - (i - 2) = 2^n - i + 1\) 种选择。所以这种情况共有 \((i-1)(2^n - i + 1)f_{i-2}\) 种。
因此 \(f_i=P_{i-1}-f_{i-1}-(i-1)(2^n - i + 1)f_{i-2}\)。
- 初始化 :\(f_0=1,f_1=0\)。
答案就是 \(\Large{\frac{f_m}{m!}}\)(不计顺序)。
P3968 TJOI2014 电源插排
维护动态开点线段树上四个信息:
- 区间最长连续空位长度
- 区间左起最长连续空位长度
- 区间右起最长连续空位长度
- 区间空位数量
赛时挂分点:
- 在左侧完全空闲时,没有让父节点的左起最长连续空位长度向右侧延伸
- 寻找最长空闲区间时,没有遵循最靠右原则,即优先查右树 -> 跨界 -> 左树
- 读错题目,当区间长度为偶数时,把插排位置理解为最靠右插排,实为中点靠右插排
- 查询区间内空闲插排数时爆栈了
cpp
#include <bits/stdc++.h>
using namespace std;
// 动态开点线段树的最大节点数,扩大以防止 MLE/RE
const int N = 6e6 + 10;
int n, q, root;
int tot = 1;
map<int, int> cnt;
struct node {
int l, r, ls, rs;
// 区间内空闲插排数量,总最长连续空闲,左起最长空闲,右起最长空闲
int siz, all, le, re;
} tree[N];
void newnode(int &rt, int l, int r) {
tree[rt].l = l;
tree[rt].r = r;
tree[rt].all = tree[rt].le = tree[rt].re = tree[rt].siz = r - l + 1;
}
void pushup(int rt) {
int ls = tree[rt].ls, rs = tree[rt].rs;
int mid = (tree[rt].l + tree[rt].r) >> 1;
// 如果子节点不存在,动态开点
if (!ls) ls = tree[rt].ls = ++tot, newnode(tree[rt].ls, tree[rt].l, mid);
if (!rs) rs = tree[rt].rs = ++tot, newnode(tree[rt].rs, mid + 1, tree[rt].r);
// 合并最长区间
tree[rt].all = max({tree[ls].all, tree[rs].all, tree[ls].re + tree[rs].le});
// 修复 Bug 1:只有左侧完全空闲,父节点的 le 才能向右侧延伸
tree[rt].le = tree[ls].le;
if (tree[ls].le == tree[ls].r - tree[ls].l + 1) tree[rt].le += tree[rs].le;
// 只有右侧完全空闲,父节点的 re 才能向左侧延伸
tree[rt].re = tree[rs].re;
if (tree[rs].re == tree[rs].r - tree[rs].l + 1) tree[rt].re += tree[ls].re;
// 合并空闲个数
tree[rt].siz = tree[ls].siz + tree[rs].siz;
}
void update(int &rt, int l, int r, int x, int v) {
if (!rt) rt = ++tot, newnode(rt, l, r);
if (l == r) {
tree[rt].all = tree[rt].le = tree[rt].re = tree[rt].siz = v;
return;
}
int mid = (l + r) >> 1;
if (x <= mid) update(tree[rt].ls, l, mid, x, v);
else update(tree[rt].rs, mid + 1, r, x, v);
pushup(rt);
}
// 修复 Bug 2:寻找最长空闲区间,优先查右树 -> 跨界 -> 左树(遵循最靠右原则)
pair<int, int> query1(int rt, int maxlen) {
// 尚未下放过的节点意味着整个区间都是完全空闲的,直接返回当前边界
if (!tree[rt].ls) {
int l = tree[rt].l, r = tree[rt].r, mid = (l + r) >> 1;
newnode((tree[rt].ls = ++tot), l, mid);
newnode((tree[rt].rs = ++tot), mid + 1, r);
return {l, r};
}
// 1. 优先查右半边(最靠右的同长区间)
if (tree[tree[rt].rs].all == maxlen) return query1(tree[rt].rs, maxlen);
// 2. 其次查跨越中心的拼接区间
if (tree[tree[rt].ls].re + tree[tree[rt].rs].le == maxlen) {
int la = tree[tree[rt].ls].r - tree[tree[rt].ls].re + 1;
int ra = tree[tree[rt].rs].l + tree[tree[rt].rs].le - 1;
return { la, ra };
}
// 3. 最后查左半边
return query1(tree[rt].ls, maxlen);
}
// 计算区间内空闲插排数
int query2(int &rt, int l, int r, int x, int y) {
if (!rt) rt = ++tot, newnode(rt, l, r);
if (tree[rt].l > y || tree[rt].r < x) return 0;
if (x <= tree[rt].l && tree[rt].r <= y) return tree[rt].siz;
int mid = (tree[rt].l + tree[rt].r) >> 1, ans = 0;
// 修复 Bug 4:用查询范围 [x, y] 去决定下落方向,避免无关节点爆栈
if (x <= mid) ans += query2(tree[rt].ls, l, mid, x, y);
if (y > mid) ans += query2(tree[rt].rs, mid + 1, r, x, y);
return ans;
}
int main() {
// 优化输入输出流
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> q;
newnode((root = 1), 1, n);
for (int i = 1, id, l, r; i <= q; i++) {
cin >> id;
if (!id) {
cin >> l >> r;
// query2 取的是"空闲",要求的答案是"已被使用",两者相减
cout << (r - l + 1) - query2(root, 1, n, l, r) << "\n";
} else {
if (cnt[id]) {
// 离开:标记位置释放为空闲(1)
update(root, 1, n, cnt[id], 1);
cnt[id] = 0;
} else {
// 进入:寻找最长空闲段
auto p = query1(root, tree[root].all);
// 修复 Bug 3:不论区间长度是奇数还是偶数,此行都能定位到居中并靠右的位置
cnt[id] = (p.first + p.second + 1) >> 1;
// 标记该位置已被占据(0)
update(root, 1, n, cnt[id], 0);
}
}
}
return 0;
}
P5234 JSOI2012 越狱老虎桥
题目大意:
给定一张图,\(A\) 先添加一条边,\(B\) 再删去一条边使得图不连通,\(A\) 要最大化删除边的权值,\(B\) 要最小化删除边的权值,问最终的权值是多少。
数据范围:\(n \le 5 \times 10^5, m \le 10^6\)。
显然,边双中的边是肯定不能删的,因为删除了也并不会使图不连通。所以我们有个自然的想法,我们可以把边双缩点,在重新建图,这样我们就简化为了树上问题。
由于敌人很聪明,他为了让我们需要出动的特工最多,一定会优先把代价最小的那些桥变成环,逼我们去切代价大的桥。
既然敌人会优先覆盖代价小的边,我们可以按照桥的代价从小到大排序 ,设为 \(e_1, e_2, e_3, \dots, e_m\)。
假设我们当前遍历到了第 \(k\) 条边,我们要判断前 \(k\) 条边能否构成一条链,如果构成就说明敌人只要在首尾加上新边就无敌了。反之如果不构成说明删掉 \(e_k\) 敌人就成小丑了。
也就是说我们要判断前 \(k\) 条边是否构成一条链。
方法很多:
- 二分 \(k\) + 暴力判链,时间复杂度为 \(O(n\log n)\)
- 依次遍历所有边,若前 \(k\) 条边所经过的结点最多只有一个结点拥有两个儿子,其余结点只拥有一个儿子,则这些边构成链,可以暴力跳、可以倍增跳,随便写,时间复杂度 \(O(n\log n)\)
- 对前 \(k\) 条边设为 \(1\),其余设为 \(0\),然后用这个边权求一遍树的直径,只需要检查直径长度是否等于 \(k\) 即可
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
const int M = 2e6 + 10;
int n, m;
int head[N], to[M], nxt[M], weight[M], edge_cnt = 0;
void add_edge(int u, int v, int w) {
to[edge_cnt] = v;
weight[edge_cnt] = w;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt++;
}
int dfn[N], low[N], tim, top;
int bri[M], dcc[N], cnt, stk[N];
void tarjan(int x, int ine) {
dfn[x] = low[x] = ++tim;
stk[++top] = x;
for (int i = head[x]; i != -1; i = nxt[i]) {
int v = to[i];
if (!dfn[v]) {
tarjan(v, i);
low[x] = min(low[x], low[v]);
if (low[v] > dfn[x])
bri[i] = bri[i ^ 1] = 1;
} else if (i != (ine ^ 1)) {
low[x] = min(low[x], dfn[v]);
}
}
if (dfn[x] == low[x]) {
++cnt;
while (true) {
int y = stk[top--];
dcc[y] = cnt;
if (x == y) break;
}
}
}
struct edge {
int u, v, w;
} Tim[N];
int tot = 0;
struct Edge {
int v, w;
};
vector<Edge> linker[N]; // 缩点后构建的树
int fa[N], dis[N], dep[N], son;
void bfs(int root) {
queue <int> q;
q.push(root);
dep[root] = 1, fa[root] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto i : linker[u]) {
int v = i.v;
if (v != fa[u]) {
fa[v] = u, dep[v] = dep[u] + 1;
q.push(v);
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
memset(head, -1, sizeof(head));
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
add_edge(u, v, w);
add_edge(v, u, w);
}
tarjan(1, -1);
for (int i = 0; i < edge_cnt; i += 2) {
if (bri[i]) {
int u = to[i ^ 1], v = to[i], w = weight[i];
linker[dcc[u]].push_back({ dcc[v], w });
linker[dcc[v]].push_back({ dcc[u], w });
Tim[++tot] = { dcc[u], dcc[v], w };
}
}
if (tot == 0) {
cout << -1 << "\n";
return 0;
}
auto work = [](const edge &x, const edge &y){ return x.w < y.w; };
sort(Tim + 1, Tim + tot + 1, work);
int root = Tim[1].u;
bfs(root);
dis[root] = Tim[1].v;
for (int i = 2; i <= tot; i++) {
int u = Tim[i].u, v = Tim[i].v;
int k = dep[u] > dep[v] ? u : v; // 取深度较深的点
// 魔法步骤:不断往上跳,将没有分配分支的祖先分配给当前路径
while (!dis[fa[k]]) {
dis[fa[k]] = k;
k = fa[k];
}
// 验证当前边能否并入已有路径,不产生冲突
if (fa[k] == root && son == 0 && dis[fa[k]] != k) {
son = k; // 合法!根节点拥有了第二个分支
}
else if (dis[fa[k]] == k || (fa[k] == root && son == k)) {
; // 合法!在同一条分支上,没有产生新的分叉
}
else {
// 产生冲突!敌人无法用一条线串起所有的边,答案就是当前这条加不进去的边
cout << Tim[i].w << "\n";
return 0;
}
}
// 所有桥都被串在了一条路径上,敌人只要把路径两端连起来,所有桥都失效,必败
cout << -1 << "\n";
return 0;
}