2025.10.9 THUSC 2022 DAY1 题解
T1.归程
题面
分析:
直接设 \(f_{t, x, 0/1}\) 表示当前为 \(t\) 时刻, 在 \(x\) 号点,没有/已经 下大雨,到达终点的最优期望。
以时间为阶段,转移枚举 \(x\) 的一条出边。
需要预处理求出 \(np_{t, l}\) 表示从 \(t\) 时刻开始算,还没有下大。经过 \(l\) 时间,还没有下大的概率。
以及 \(fa_{t, l}, f_{b, l}\) 表示从 \(t\) 时刻开始(\(t\) 时刻还没下大),经过 \(l\) 后已经下大, \(a\), \(b\) 的系数。
然后可以 \(O(1)\) 转移。
复杂度 \(O(N \times l \times M)\)。
CODE:
// f[t][x][0/1] 表示t時刻,當前在 x 號點, 沒有/有下大到達終點的最優期望值
#include<bits/stdc++.h>
using namespace std;
const int T = 20100;
const int N = 1010;
const int M = 4010;
typedef double db;
const db INF = 1e18;
int n, m, K, s, t;
int tim[N], idx[N];
db P[N], w[N], suf_w[N];
db f[T][N][2], p[T][25], np[T][25]; // p[t][i] 表示還沒有下大,從第t秒開始往後i秒下大的概率
db fa[T][25], fb[T][25]; // 係數
struct edge {
int v, last;
int l; db a, b;
}E[M * 2];
int head[N], tot;
int bigger[T];
void add(int u, int v, int l, db a, db b) {
E[++ tot].v = v;
E[tot].l = l;
E[tot].a = a;
E[tot].b = b;
E[tot].last = head[u];
head[u] = tot;
}
void pre() { // 首先求出 p 數組
for(int t = 0; t < T - 80; t ++ ) { // 枚舉一個開始時刻
double pp = 1; // 不下大的概率
int now = t + 1;
for(int l = 1; l <= 20; l ++ ) { // 從 t 開始算
if(bigger[now]) {
p[t][l] = pp * (db)(w[bigger[now]] / suf_w[bigger[now]]);
pp = pp * (1.0 - (db)(w[bigger[now]] / suf_w[bigger[now]]));
}
else p[t][l] = 0;
np[t][l] = pp;
now ++;
}
}
// 接着求出係數數組
for(int t = 0; t < T - 80; t ++ ) {
for(int l = 1; l <= 20; l ++ ) { // 枚舉一個時長
for(int k = 0; k <= l; k ++ ) { // 枚舉變化的時刻
fa[t][l] += p[t][k] * (db)(1.0 * k);
fb[t][l] += p[t][k] * (db)(1.0 * l - k);
}
}
}
}
int main() {
scanf("%d%d%d%d%d", &n, &m, &K, &s, &t);
for(int i = 1; i <= m; i ++ ) {
int u, v, l; db a, b;
scanf("%d%d%d%lf%lf", &u, &v, &l, &a, &b);
add(u, v, l, a, b);
add(v, u, l, a, b);
}
db sw = 0;
for(int i = 1; i <= K; i ++ ) {
scanf("%d%lf", &tim[i], &w[i]);
bigger[tim[i]] = i;
sw += w[i];
}
for(int i = K; i >= 1; i -- ) suf_w[i] = suf_w[i + 1] + w[i]; // 求后缀和
for(int i = 1; i <= K; i ++ ) P[i] = w[i] / sw;
pre();
int lim = 20 * n;
for(int i = 0; i < T; i ++ ) {
for(int x = 1; x <= n; x ++ ) {
f[i][x][0] = f[i][x][1] = INF;
}
}
for(int i = 0; i <= lim; i ++ ) {
f[i][t][0] = f[i][t][1] = 0;
}
for(int i = lim; i >= 0; i -- ) { // 枚舉一個時刻
for(int x = 1; x <= n; x ++ ) { // 枚舉一個地點
for(int j = head[x]; j; j = E[j].last) { // 枚舉一個出去的點
int y = E[j].v, l = E[j].l; db a = E[j].a, b = E[j].b;
f[i][x][0] = min(f[i][x][0], (f[i + l][y][0] + a * l) * np[i][l] + f[i + l][y][1] * (1.0 - np[i][l]) + fa[i][l] * a + fb[i][l] * b);
f[i][x][1] = min(f[i][x][1], f[i + l][y][1] + b * l);
}
}
}
printf("%.7lf\n", f[0][s][0]);
return 0;
}
T2.最大连续和
题面
题意:
给你一个长度为 \(n\) 的数组 \(a_i\) 和 长度为 \(m_i\) 的数组 \(b\)。你可以将一个 \(a_i\) 置换成任意一个 \(b_j\),但是每个 \(b_j\) 最多只能被用一次。求所有置换方案的最大子段和。
数据规模:
\(1 \leq n \leq 10^5\), \(0 \leq m \leq 10^5\),\(-10^9 \leq a_i, b_i \leq 10^9\)。
分析:
记 \(f(l, r)\) 表示用 \(b\) 置换 \([l, r]\) 区间的数,能够得到 \([l, r]\) 区间 的 最大区间和。
如果我们会求 \(f(l, r)\),那么暴力就是 \(O(n^2)\) 枚举一个区间然后将 \(f(l, r)\) 取 \(max\)。
对于一个左端点 \(l\),我们考虑快速找到一个最优的右端点 \(r_l\)。
不难发现当 \(l\) 递增时, \(r_l\) 单调不降。具有 决策单调性。
因此可以分治,每一层处理最中间 \(l\),最多递归 \(log\) 层,因此最多求 \(n\times \log_2n\) 次 \(f(l, r)\)。
求 \(f(l, r)\) :将 \(b\) 从大到小排序,相当于要找到 \([l, r]\) 区间的第 \(k\) 小数 \(x\),使得 \(x \geq b_{k}\)。这个可以在 主席树 上二分做到一个 \(\log\)。
总复杂度 \(O(n\times \log^2_2{n})\)。
CODE:
#include<bits/stdc++.h>
#define MP make_pair
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
typedef pair< int, LL > PII;
int n, m;
LL a[N], b[N], Sa[N], Sb[N], res = -1e18;
bool cmp(LL x, LL y) {return x > y;}
struct SegmentTree {
int ls, rs, cnt;
LL sum;
#define ls(x) t[x].ls
#define rs(x) t[x].rs
#define cnt(x) t[x].cnt
#define sum(x) t[x].sum
}t[N * 32];
int tot, root[N];
int build() {return ++ tot;}
void update(int p) {
cnt(p) = cnt(ls(p)) + cnt(rs(p));
sum(p) = sum(ls(p)) + sum(rs(p));
}
int ins(int p, int lp, int rp, int pos) {
int tp = build(); t[tp] = t[p];
if(lp == rp) {
cnt(tp) ++;
sum(tp) += 1LL * lp;
return tp;
}
int mid = (lp + rp >> 1);
if(pos <= mid) ls(tp) = ins(ls(tp), lp, mid, pos);
else rs(tp) = ins(rs(tp), mid + 1, rp, pos);
update(tp);
return tp;
}
PII ask(int p, int q, int lp, int rp, int now, LL s) {
if(lp == rp) {
if(cnt(p) - cnt(q) == 0) return MP(now - 1, s);
else {
int l = now, r = m, mid, res = -1;
while(l <= r) {
mid = (l + r >> 1);
if(b[mid] > lp) res = mid, l = mid + 1;
else r = mid - 1;
}
if(res == -1) return MP(now - 1, s);
res = min(res, now + cnt(p) - cnt(q) - 1);
return MP(res, s + 1LL * (res - now + 1) * lp);
}
}
int mid = (lp + rp >> 1);
int lsz = cnt(ls(p)) - cnt(ls(q));
if((now + lsz > m) || (b[now + lsz] <= mid)) return ask(ls(p), ls(q), lp, mid, now, s);
else return ask(rs(p), rs(q), mid + 1, rp, now + lsz, s + sum(ls(p)) - sum(ls(q)));
}
LL f(int l, int r) { // 计算 [l, r] 区间的最优值
PII subs = ask(root[r], root[l - 1], -1e9, 1e9, 1, 0);
return Sa[r] - Sa[l - 1] + (Sb[subs.first] - subs.second);
}
void calc(int lq, int rq, int lp, int rp) {
if(lq > rq) return ;
int mid = (lq + rq >> 1);
LL ans = -1e18; int rm;
for(int i = max(lp, mid); i <= rp; i ++ ) {
LL v = f(mid, i);
if(v > ans) {ans = v; rm = i;}
}
res = max(res, ans);
calc(lq, mid - 1, lp, rm); calc(mid + 1, rq, rm, rp);
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ ) {
scanf("%lld", &a[i]);
Sa[i] = Sa[i - 1] + a[i];
}
for(int i = 1; i <= m; i ++ )
scanf("%lld", &b[i]);
sort(b + 1, b + m + 1, cmp);
for(int i = 1; i <= m; i ++ ) Sb[i] = Sb[i - 1] + b[i];
for(int i = 1; i <= n; i ++ ) {
root[i] = ins(root[i - 1], -1e9, 1e9, a[i]);
}
calc(1, n, 1, n);
printf("%lld\n", res);
return 0;
}
T3.寻宝
题面
题意:
通信题。
有一张 \(n\) 个点, \(m\) 条边的无向图。其中有一个点 \(x\) 是宝藏点。并且 保证图没有奇环。
你需要实现两个函数完成通信:
- \(Alice\) 函数:给你所有的边,用 \((u, v)\) 表示。并且你知道藏宝点的编号 \(x\),你需要给每条边 定向,让它指向 \(u\) 或者指向 \(v\)。以此来为 \(Bob\) 提供信息。将所有边定向后 点的编号将会重新随机生成。(你并不知道重新生成后原来每个点的编号)
- \(Bob\) 函数:你可以调用
vector< pair< int, bool > > discover(int pos);函数得到编号为 \(pos\) 的点所有连边的点 \(v\),已经这条边的方向。你需要在不超过 \(5000\) 次找到藏宝点的编号 \(x'\),并返回 \(x'\)。
数据规模:
\(n \leq 10^6, m \leq 2\times 10^6\)
分析:
感觉通信题的思路都很智慧。
首先有一个想法是在 \(Alice\) 函数中以 \(x\) 为起点,向四周 \(bfs\) 一遍得到每个点的深度。然后将每一条边由深度大的指向深度小的,这样就得到了一个询问次数 \(O(n)\) 的做法。
然后是考虑正解:
我们考虑设置一个阈值 \(B\),每次随机一个点,判断这个点的深度是否小于等于 \(B\)。如果是那么能够在 \(B\) 次到藏宝点。如果不是就接着随机,那么期望随机次数是 \(\frac{n}{B}\)。如果能够询问一次判断出这个点的深度是否小于等于 \(B\),当 \(B\) 取 \(\sqrt{n}\) 时期望询问次数就是 \(2\sqrt{n}\)。
由于没有奇环,因此图是一张 二分图。 考虑从 \(x\) 开始将所有点黑白染色。那么 所有深度为奇数的点都只会与深度为偶数的点相连,所有深度为偶数的点也只会和深度为奇数的点相连。
对于一条边 \((u, v)\):
如果 \(u, v\) 的深度都小于等于 \(B\),那么我们令其指向深度较小的点。
否则我们令其指向 深度为奇数 的点。(二分图的作用是保证两个点的深度一定是一个奇数,一个偶数)
那么一个点的深度如果大于 \(B\),它的出度或者入度一定有一个为 \(0\)。如果随机到了这样的点,我们就接着随。
有一个细节是 菊花 可能会导致所有点都是出度或入度有一个为 \(0\),因此当随机次数超过 \(n / B\) 时应当直接 \(break\)。
CODE:
// 妙妙题
#include "treasure.h"
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 1e6 + 10;
int B;
mt19937_64 rnd(time(0));
int dep[N];
bool vis[N], bok[N];
vector< int > Gra[N];
vector< pair< int, bool > > vec[N];
int ran(int l, int r) {return rnd() % (r - l + 1) + l;}
void bfs(int s) {
dep[s] = 1;
queue< int > q;
q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
for(auto v : Gra[u]) {
if(dep[v]) continue;
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
vector< pair< int, bool> > discover(int pos);
void Alice(const int testid, const int n, const int m, const int x, const int u[], const int v[], bool dir[]) {
B = sqrt(n);
for(int i = 0; i < m; i ++ ) {
Gra[u[i]].pb(v[i]);
Gra[v[i]].pb(u[i]);
}
bfs(x);
for(int i = 0; i < m; i ++ ) {
int nu = u[i], nv = v[i];
bool f = 1;
if(dep[nu] > dep[nv]) swap(nu, nv), f ^= 1;
if(dep[nv] <= B) dir[i] = f;
else {
if(dep[nv] & 1) dir[i] = f;// 奇数向外
else dir[i] = (f ^ 1);
}
}
}
int Bob(const int testid, const int n) {
B = sqrt(n);
int s = -1;
int cnt = 0;
while(1) { // 随机到一个起点
s = ran(0, n - 1);
if(vis[s]) {cnt ++; continue;}
if(!vis[s]) {
vis[s] = 1;
vec[s] = discover(s);
}
int in = 0, ou = 0;
for(auto v : vec[s]) {
if(v.second) in ++;
else ou ++;
}
if(in && ou) break;
cnt ++;
if(cnt >= 2.5 * (n / B) + 10) break;
}
while(1) { // 从 s 出发
if(!vis[s]) {
vis[s] = 1;
vec[s] = discover(s);
}
int ouid = -1;
for(auto v : vec[s]) {
if(!v.second) ouid = v.first;
}
if(ouid == -1) return s;
s = ouid;
}
}
T4. 字符树
分析:
VP 的时候糖了。
树剖。
对每个点维护向上长度为 \(X\) 的字符串的hash值。
每次查询时,对于完整区间都在重链上的点,直接在线段树里面查。对于区间会跳链的点,由于重链只有 \(log\) 条,暴力合并上一条链的若干位置,然后跑 \(KMP\)。
对于修改:对每条重链收到影响的点跑一遍 \(KMP\) 重新插进去。
查 区间里某一种数的个数,写一个 线段树套 \(hash\) 表 即可。
复杂度 \(O(m\log^2_2n + mX\log_2n)\)。
代码没写。

浙公网安备 33010602011771号