T1. 搬箱子
Description
给出一个长度为 (n) 的序列 (a),和一个限制值 (m)。
每一轮需要取走一些数:
- 在取走的数之和不超过限制值 (m) 的前提下,取走的数尽量多。
- 在取走的数尽量多的情况下,取走的数的下标形成的序列的字典序尽量大。
在满足上述取数方案的情况下,一共会进行多少轮?
数据范围:(1 leq n leq 5 imes 10^4),(1 leq m, a_i leq 10^9)。
Solution
莽!
模拟每一轮取数的过程。
在前几轮取数都没有问题的时候,显然贪心地在剩下的数中从最小的数一个一个选起,可以得到该轮取走的数的数目,不妨记这个量为 (k)。
然后可以一位一位地确定这 (k) 个取走的数的下标。
假设当前已经钦定了前 (t) 个取走的数的下标 (p_1, p_2, cdots, p_t)。
因为要满足字典序尽量大,这个 (p_{t + 1}) 在满足题目要求的条件下一定是越靠后越好。
那么此时选定的 (p_{t + 1}) 需要满足:在区间 ([p_{t + 1}, n]) 中前 (k - t) 小的数的和不超过 (m - sumlimits_{i = 1}^{t }a_{p_i})。
那么二分答案即可。
使用的数据结构需要支持:单点修改 + 后缀前 (k) 小查询。
可以使用树状数组套权值线段树,树状数组维护后缀。
修改和查询的时间复杂度都是 (mathcal{O}(log^2 n)),二分答案还有一个 (mathcal{O}(log n))。
于是总的时间复杂度为 (mathcal{O}(n log^3 n))。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 50010, MLOGNLOGN = 20001000;
const int SIZE = 1e9 + 10;
int n, m;
int a[N];
multiset<int> s;
int cT, root[N];
struct SegmentTree {
int lc, rc;
int cnt;
long long sum;
} t[MLOGNLOGN];
int New() {
int p = ++ cT;
t[p].lc = t[p].rc = t[p].cnt = t[p].sum = 0;
return p;
}
void insert(int &p, int l, int r, int delta, int Cv, int Sv) {
if (!p) p = New();
t[p].cnt += Cv, t[p].sum += Sv;
if (l == r) return;
int mid = (l + r) >> 1;
if (delta <= mid)
insert(t[p].lc, l, mid, delta, Cv, Sv);
else
insert(t[p].rc, mid + 1, r, delta, Cv, Sv);
}
void add(int x, int delta, int Cv, int Sv) {
x = n - x + 1;
for (; x <= n; x += x & -x)
insert(root[x], 1, SIZE, delta, Cv, Sv);
}
vector<int> rt;
void prework(int x) {
x = n - x + 1;
for (int i = rt.size() - 1; i >= 0; i --) rt.pop_back();
for (; x; x -= x & -x)
rt.push_back(root[x]);
}
long long ask(int l, int r, int k) {
if (l == r)
return 1ll * k * l;
int mid = (l + r) >> 1;
int lcnt = 0;
long long lsum = 0;
for (int i = 0; i < rt.size(); i ++)
lcnt += t[t[rt[i]].lc].cnt,
lsum += t[t[rt[i]].lc].sum;
if (k <= lcnt) {
for (int i = 0; i < rt.size(); i ++) rt[i] = t[rt[i]].lc;
return ask(l, mid, k);
} else {
for (int i = 0; i < rt.size(); i ++) rt[i] = t[rt[i]].rc;
return ask(mid + 1, r, k - lcnt) + lsum;
}
}
vector<int> recover;
void round() {
int L = m;
int k = 0;
while (s.size() && (*s.begin()) <= L) {
int val = *s.begin();
recover.push_back(val);
s.erase(s.begin());
L -= val;
k ++;
}
for (int i = recover.size() - 1; i >= 0; i --) {
int val = recover[i];
s.insert(val);
recover.pop_back();
}
int res = m;
int last = 0;
for (int t = 1; t <= k; t ++) {
int l = last + 1, r = n;
while (l < r) {
int mid = (l + r + 1) >> 1;
prework(mid);
if (ask(1, SIZE, k - t + 1) <= res) l = mid; else r = mid - 1;
}
last = l;
res -= a[l];
s.erase(s.find(a[l]));
add(l, a[l], -1, -a[l]);
}
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i ++)
a[i] = read();
for (int i = 1; i <= n; i ++)
s.insert(a[i]);
for (int i = 1; i <= n; i ++)
add(i, a[i], 1, a[i]);
int ans = 0;
while (s.size())
round(), ans ++;
printf("%d
", ans);
return 0;
}
T2. 白兰地厅的西瓜
Description
给出一棵 (n) 个点的树,每个点都有一个权值 (a_i)。
可以在树上选定一个起点 (S) 和一个终点 (T)。
你需要最大化从 (S) 到 (T) 的简单路径上所有点按顺序组成的权值序列的最长上升子序列的长度。
数据范围:(1 leq n leq 10^5),(1 leq a_i leq 10^9)。
Solution
比较一眼吧 ...
可以枚举 (S) 到 (T) 的简单路径上最高(深度最小)的那个点 (p)。
那么这个简单路径可以被拆成 (p) 以及从 (p) 开始往下走的两条路径,一条路径包含了 (S),一条路径包含了 (T)。
分两种情况:
- 答案包含 (p):则包含 (S) 的链所取出的 LIS 均小于 (a_p),包含 (T) 的链所取出的 LIS 均大于 (a_p),两链内部均递增。
- 答案不包含 (p):则必定可以找到一个数 (H),使得包含 (S) 的链所取出的 LIS 均小于 (H),包含 (T) 的链所取出的 LIS 均大于 (H),两链内部均递增。
可以考虑线段树合并。
权值线段树的每个节点上需要维护 (st, ed):分别表示以权值在 ([l, r]) 内的点为起点的 LIS 的最大长度(最长下降链),以权值在 ([l, r]) 内的点为终点的 LIS 的最大长度(最长上升链)。
对于第一种情况,直接在遍历的时候查询线段树更新即可。
对于第二种情况,需要在线段树合并的时候更新答案。
时间复杂度 (mathcal{O}(n log n))。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 100100, MLOGN = 10001000;
const int SIZE = 1e9 + 1;
int n;
int a[N];
int tot, head[N], ver[N * 2], Next[N * 2];
void add(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
int ans = 1;
int cT, root[N];
struct SegmentTree {
int lc, rc;
int st, ed;
SegmentTree() {
lc = rc = 0;
st = ed = 0;
}
} t[MLOGN];
int New() {
int p = ++ cT;
t[p].lc = t[p].rc = 0;
t[p].st = t[p].ed = 0;
return p;
}
void insert(int &p, int l, int r, int delta, int val, int type) {
if (!p) p = New();
if (type == 1) t[p].st = max(t[p].st, val);
else t[p].ed = max(t[p].ed, val);
if (l == r) return;
int mid = (l + r) >> 1;
if (delta <= mid)
insert(t[p].lc, l, mid, delta, val, type);
else
insert(t[p].rc, mid + 1, r, delta, val, type);
}
SegmentTree ask(int p, int l, int r, int s, int e) {
if (s <= l && r <= e)
return t[p];
int mid = (l + r) >> 1;
SegmentTree self, lc, rc;
if (s <= mid) {
lc = ask(t[p].lc, l, mid, s, e);
self.st = max(self.st, lc.st);
self.ed = max(self.ed, lc.ed);
}
if (mid < e) {
rc = ask(t[p].rc, mid + 1, r, s, e);
self.st = max(self.st, rc.st);
self.ed = max(self.ed, rc.ed);
}
return self;
}
int merge(int p, int q) {
if (!p || !q)
return p ^ q;
ans = max(ans, t[t[p].lc].ed + t[t[q].rc].st);
ans = max(ans, t[t[q].lc].ed + t[t[p].rc].st);
t[p].lc = merge(t[p].lc, t[q].lc);
t[p].rc = merge(t[p].rc, t[q].rc);
t[p].st = max(t[p].st, t[q].st);
t[p].ed = max(t[p].ed, t[q].ed);
return p;
}
void dfs(int u, int fa) {
int St = 0, Ed = 0;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == fa) continue;
dfs(v, u);
int valS = ask(root[v], 0, SIZE, a[u] + 1, SIZE).st;
int valT = ask(root[v], 0, SIZE, 0, a[u] - 1).ed;
St = max(St, valS);
Ed = max(Ed, valT);
ans = max(ans, ask(root[u], 0, SIZE, 0, a[u] - 1).ed + valS + 1);
ans = max(ans, ask(root[u], 0, SIZE, a[u] + 1, SIZE).st + valT + 1);
root[u] = merge(root[u], root[v]);
}
insert(root[u], 0, SIZE, a[u], St + 1, 1);
insert(root[u], 0, SIZE, a[u], Ed + 1, 2);
}
int main() {
n = read();
for (int i = 1; i <= n; i ++)
a[i] = read();
for (int i = 1; i < n; i ++) {
int u = read(), v = read();
add(u, v), add(v, u);
}
dfs(1, 0);
printf("%d
", ans);
return 0;
}
T3. Emiya 家明天的饭
Description
有 (n) 个人,以及 (m) 盘菜。
其中,第 (i) 个人对第 (j) 盘菜的评价为 (a_{i, j})。
特别地,如果 (a_{i, j} = -1),则表示第 (i) 个人不喜欢第 (j) 盘菜。
若某个人在餐桌上看到了自己不喜欢的菜,那么他会被气走。
在确定了上菜方案的情况下。
若第 (i) 个人在场,第 (j) 盘菜也在场,那么你就会获得 (a_{i, j}) 的收益。
请确定一个上菜方案,使得你获得的收益最大化。
数据范围:(1 leq n leq 20),(1 leq m leq 10^6)。
Solution
考虑枚举最后在场的人的集合 (S),设在场的人的集合为 (S) 的时候的收益为 (f(S))。
因为不容易直接求出所有 (f) 值,所以考虑构造一个辅助函数 (g),满足:
对于第 (i) 个人对第 (j) 盘菜的评价 (a_{i, j} geq 0):
如果第 (j) 道菜最终在场,那么 (S) 一定是 (t_j) 的子集,即 (S subseteq t_j)。
上述操作的第一步是:计算所有不排斥第 (j) 盘菜的人的贡献。
上述操作的第二步是:扣除所有不排斥第 (j) 盘菜,且不在场的人的贡献。
这样即可确保贡献正确计算。
知道了 (g) 之后即可还原 (f)。
时间复杂度 (mathcal{O}(2^nn + nm))。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 21, M = 1001000;
int n, m;
int a[N][M];
int t[M];
int f[1 << N];
int main() {
n = read(), m = read();
for (int i = 0; i < n; i ++)
for (int j = 1; j <= m; j ++)
a[i][j] = read();
for (int i = 0; i < n; i ++)
for (int j = 1; j <= m; j ++)
if (a[i][j] >= 0) t[j] |= (1 << i);
for (int i = 0; i < n; i ++)
for (int j = 1; j <= m; j ++)
if (a[i][j] >= 0) {
f[t[j]] += a[i][j];
f[t[j] ^ (1 << i)] -= a[i][j];
}
for (int i = 0; i < n; i ++)
for (int S = 0; S < (1 << n); S ++)
if (S & (1 << i)) f[S ^ (1 << i)] += f[S];
int ans = 0;
for (int S = 0; S < (1 << n); S ++)
ans = max(ans, f[S]);
printf("%d
", ans);
return 0;
}
T4. 种树
(待填 ...)