「SCOI2015」小凸玩密室
题意
小凸和小方相约玩密室逃脱,这个密室是一棵有 $ n $ 个节点的完全二叉树,每个节点有一个灯泡。点亮所有灯泡即可逃出密室。每个灯泡有个权值 $ A_i $,每条边也有个权值 $ B_i $ 。
点亮第 $ 1 $ 个灯泡不需要花费,之后每点亮一个新的灯泡 $ V $ 的花费,等于上一个被点亮的灯泡 $ U $ 到这个点 $ V $ 的距离 $ D(u, v) $,乘以这个点的权值 $ A_v $。
在点灯的过程中,要保证任意时刻所有被点亮的灯泡必须连通,在点亮一个灯泡后必须先点亮其子树所有灯泡才能点亮其他灯泡。请告诉他们,逃出密室的最少花费是多少。
$ 1 leq n leq 2 imes 10 ^ 5, 1 < A_i, B_i leq 10 ^ 5 $
题解
参考了 zzzzx 大佬的博客 。
完全二叉树的性质是很优秀的,主要有两点。
- 树高不超过 (lfloor log_2 n floor) 。
- 对于每个节点,最多只存在一个兄弟节点。
根据这两条性质我们可以去考虑如何解决。
首先认真读题 ,一开始任选起点,任意时刻点亮的灯泡是联通的,并且子树需要点完。
所以我们点完一个点后,下一步,要么回到它的一个祖先,要么回到它祖先的一个儿子。
由于树高是 (O(log n)) 的,所以我们
令 f[i][j]
为考虑完 (i) 子树的节点,回到它 (j) 级祖先,所需要的最小花费。
令 g[i][j]
为考虑完 (i) 子树的节点,回到它 (j) 级祖先的另外一个儿子,所需要的最小花费。
为了方便,我们预处理 dis[i][j]
为 (i) 到 (j) 级祖先的距离。
规定 anc(i, j)
为 (i) 的 (j) 级祖先,bro(i, j)
为 (i) 的 (j) 级祖先的另外一个儿子。(这些都可以通过二进制表达)ls
为左儿子,rs
为右儿子。
转移的话,分三种情况讨论。
-
(i) 为叶子节点,那么它只能向父亲走,可以直接计算。
f[i][j] = dis[i][j] * a[anc(i, j)]; g[i][j] = (dis[i][j] + dis[bro(i, j)][1]) * a[bro(i, j)];
-
(i) 只有一个儿子。(只可能为左儿子)然后考虑从左儿子直接转移上来即可。
f[i][j] = f[ls][j + 1] + dis[ls][1] * a[ls]; g[i][j] = g[ls][j + 1] + dis[ls][1] * a[ls];
-
(i) 有两个儿子,那直接枚举它走两个儿子的先后顺序就行了。
f[i][j] = min( g[ls][1] + dis[ls][1] * a[ls] + f[rs][j + 1], g[rs][1] + dis[rs][1] * a[rs] + f[ls][j + 1]); g[i][j] = min( g[ls][1] + dis[ls][1] * a[ls] + g[rs][j + 1], g[rs][1] + dis[rs][1] * a[rs] + g[ls][j + 1]);
最后枚举起点,暴力考虑就行了。
每次只需要一直向上走,然后走上去的时候记得还要走下另外一个儿子,然后要走回来。
具体实现如下:
ll tmp = f[x][1];
for (int u = x >> 1, v = x ^ 1; ; u >>= 1, (v >>= 1) ^= 1) {
tmp += (v <= n) ? (dis[v][1] * a[v] + f[v][2]) : dis[u][1] * a[u >> 1];
if (!u) break ;
} // tmp 为答案
总结
对于完全二叉树有许多优美的性质可以使用。
并且一定要注意认真读题!!!
代码
总体来说还是比较好写的。
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
using namespace std;
typedef long long ll;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2009.in", "r", stdin);
freopen ("2009.out", "w", stdout);
#endif
}
const int N = 2e5 + 1e3;
#define anc(x, y) (x >> y)
#define bro(x, y) ((x >> y - 1) ^ 1)
#define ls (i << 1)
#define rs (i << 1 | 1)
int a[N], n;
ll f[N][20], g[N][20], dis[N][20];
int main () {
File();
n = read();
For (i, 1, n) a[i] = read();
For (i, 2, n) {
dis[i][1] = read();
for (int j = 2; anc(i, j); ++ j)
dis[i][j] = dis[i >> 1][j - 1] + dis[i][1];
}
Fordown (i, n, 1) for (int j = 1; ; ++ j) {
if (ls > n) {
f[i][j] = dis[i][j] * a[anc(i, j)];
g[i][j] = (dis[i][j] + dis[bro(i, j)][1]) * a[bro(i, j)];
} else if (rs > n) {
f[i][j] = f[ls][j + 1] + dis[ls][1] * a[ls];
g[i][j] = g[ls][j + 1] + dis[ls][1] * a[ls];
} else {
f[i][j] = min(
g[ls][1] + dis[ls][1] * a[ls] + f[rs][j + 1],
g[rs][1] + dis[rs][1] * a[rs] + f[ls][j + 1]);
g[i][j] = min(
g[ls][1] + dis[ls][1] * a[ls] + g[rs][j + 1],
g[rs][1] + dis[rs][1] * a[rs] + g[ls][j + 1]);
}
if (!anc(i, j)) break ;
}
ll ans = 1e18;
For (x, 1, n) {
ll tmp = f[x][1];
for (int u = x >> 1, v = x ^ 1; ; u >>= 1, (v >>= 1) ^= 1) {
tmp += (v <= n) ? (dis[v][1] * a[v] + f[v][2]) : dis[u][1] * a[u >> 1];
if (!u) break ;
}
chkmin(ans, tmp);
}
printf ("%lld
", ans);
return 0;
}
「SCOI2015」小凸解密码
题意
小凸得到了一个密码盘,密码盘被等分成 $ N $ 个扇形,每个扇形上有一个数字($ 0 sim 9 $),和一个符号(+
或 *
),密码盘解密的方法如下:
- 首先,选择一个位置开始,顺时针地将数字和符号分别记在数组 $ A $ 和数组 $ C $ 中;
- $ B_0 = A_0 $
- 当 $ x > 0 (() x $ 为下标值)时:
- 若 $ C_x $ 为
+
,$ B_x = (A_x + A_{x - 1}) mod 10 $; - 若 $ C_x $ 为
*
,$ B_x = (A_x imes A_{x - 1}) mod 10 $;
操作完成后,可以得到一个长度为 $ n $ 的数组 $ B $,然后以 $ B_0 $ 为起点将 $ B $ 数组顺时针写成一个环,解密就完成了,称得到的环为答案环。
现在小凸得到了一份指令表,指令表上有 $ 2 $ 种操作。
- 一种指令是修改操作,即改变原来密码盘上一个位置的数字和符号;
- 另一种指令是询问操作,具体如下:
- 首先从指令给出的位置开始完成解密,得到答案环;
- 答案环上会有一些 $ 0 $ 连在一起,将这些连在一起的 $ 0 $ 称为零区间,找出其中距离 $ B_0 $ 最远的那个零区间,输出这个距离;
- 零区间和 $ B_0 $ 的距离定义为:零区间内所有 $ 0 $ 到 $ B_0 $ 距离中的最小值。
$ 5 leq n, m leq 10 ^ 5 $
题解
不难发现每次操作和询问最多只会改变周围两个点的状态,我们每次暴力改变状态就行了。
然后考虑用 std :: set<pair<int, int>>
维护所有全零区间。
每次插入的时候把相邻的合并,删除的时候拿出来拆开就行了。
查询的时候,只需要先查它离它对立面( (displaystyle B_0 + frac{n}{2}) )最近的三四个区间就行了。(怕少找了就多搞了几个区间)
然后依次考虑距离就行了,最后找最长距离的就行了。
注意它是个环,算距离以及找相邻区间的时候要考虑上首尾区间!!
复杂度是 (O((n + m) log n)) 的,随机数据跑的很快(全零区间较少)。
代码
细节有点多,建议参考代码。
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for(register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
#define fir first
#define sec second
#define mp make_pair
using namespace std;
typedef pair<int, int> PII;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
inline char read_opt() {
char ch(getchar());
for (; ch != '+' && ch != '*'; ch = getchar());
return ch;
}
void File() {
#ifdef zjp_shadow
freopen ("2010.in", "r", stdin);
freopen ("2010.out", "w", stdout);
#endif
}
const int N = 1e5 + 1e3;
int n; set<PII> S;
inline void Insert(int pos) {
int l = pos, r = pos;
auto nex = S.lower_bound(mp(pos, 0));
if (nex != S.begin()) {
auto pre = prev(nex);
if (pre != S.end() && pre -> sec == l - 1) l = pre -> fir, S.erase(pre);
}
if (nex != S.end() && nex -> fir == r + 1) r = nex -> sec, S.erase(nex);
S.emplace(l, r);
}
inline void Delete(int pos) {
auto Seg = prev(S.upper_bound(mp(pos, n)));
int l = Seg -> fir, r = Seg -> sec;
assert(l <= pos && pos <= r); S.erase(Seg);
if (l < pos) S.emplace(l, pos - 1);
if (r > pos) S.emplace(pos + 1, r);
}
inline int Len(PII a, int b) {
if (a.fir <= b && b <= a.sec) return 0;
return min( {abs(a.fir - b), n - abs(a.fir - b), abs(a.sec - b), n - abs(a.sec - b)} );
}
#define Dis(a) Len(a, pos)
inline int Calc(int pos, PII Seg) {
int dis = Dis(Seg);
if (Seg.fir == 0) {
auto End = prev(S.end());
if (End -> sec == n - 1) chkmin(dis, Dis(*End));
}
if (Seg.sec == n - 1) {
auto Beg = S.begin();
if (Beg -> fir == 0) chkmin(dis, Dis(*Beg));
}
return dis;
}
inline int Find(int pos) {
if (S.empty()) return - 1;
int ans = 0;
auto cur = S.lower_bound(mp((pos + n / 2) % n, (pos + n / 2) % n));
if (cur == S.end()) cur = prev(cur);
if (cur != S.end()) chkmax(ans, Calc(pos, *cur));
auto pre = cur != S.begin() ? prev(cur) : prev(S.end());
chkmax(ans, Calc(pos, *pre));
pre = pre != S.begin() ? prev(pre): prev(S.end());
chkmax(ans, Calc(pos, *pre));
auto nex = next(cur);
if (nex == S.end()) nex = S.begin();
chkmax(ans, Calc(pos, *nex));
return ans;
}
int m;
char opt[N]; int val[N], res[N];
inline void Update(int pos, bool flag = false) {
if (pos == n) return ;
int bef = res[pos];
if (flag) res[pos] = val[pos];
else {
if (opt[pos] == '+') res[pos] = (val[pos] + val[(pos - 1 + n) % n]) % 10;
if (opt[pos] == '*') res[pos] = (val[pos] * val[(pos - 1 + n) % n]) % 10;
}
if (~bef) {
if (!bef && res[pos]) Delete(pos);
if (bef && !res[pos]) Insert(pos);
} else if (!res[pos]) Insert(pos);
}
int main () {
File();
n = read(); m = read();
Set(res, -1);
Rep(i, n)
val[i] = read(), opt[i] = read_opt(), Update(i, !i);
Rep(i, m) {
int type = read(), pos = read();
if (type == 1) {
val[pos] = read(), opt[pos] = read_opt();
Update(pos); Update(pos + 1);
} else {
if (pos) Update(0, false), Update(pos, true);
printf ("%d
", Find(pos));
if (pos) Update(pos, false), Update(0, true);
}
}
return 0;
}
「SCOI2015」情报传递
题意
奈特公司是一个巨大的情报公司,它有着庞大的情报网络。情报网络中共有 $ n $ 名情报员。每名情报员可能有若干名(可能没有)下线,除 $ 1 $ 名大头目外其余 $ n - 1 $ 名情报员有且仅有 $ 1 $ 名上线。奈特公司纪律森严,每名情报员只能与自己的上、下线联系,同时,情报网络中仟意两名情报员一定能够通过情报网络传递情报。
奈特公司每天会派发以下两种任务中的一个任务:
- 搜集情报:指派 $ T $ 号情报员搜集情报;
- 传递情报:将一条情报从 $ X $ 号情报员传递给 $ Y $ 号情报员。
情报员最初处于潜伏阶段,他们是相对安全的,我们认为此时所有情报员的危险值为 $ 0 $;一旦某个情报员开始搜集情报,他的危险值就会持续增加,每天增加 $ 1 $ 点危险值(开始搜集情报的当天危险值仍为 $ 0 $,第 $ 2 $ 天危险值为 $ 1 $,第 $ 3 $ 天危险值为 $ 2 $,以此类推)。传递情报并不会使情报员的危险值增加。
为了保证传递情报的过程相对安全,每条情报都有一个风险控制值 $ C $。余特公司认为,参与传递这条情报的所有情报员中,危险值大于 $ C $ 的情报员将对该条情报构成威胁。现在,奈特公司希望知道,对于每个传递情报任务,参与传递的情报员有多少个,其中对该条情报构成威胁的情报员有多少个。
$ n leq 2 imes 10 ^ 5, Q leq 2 imes 10 ^ 5, 0 < P_i, C_i leq N, 1 leq T_i, X_i, Y_i leq n $
题解
很简单的一道题。
第一问就是求树上两点距离,十分思博。(用倍增求就行了,比较好写,我竟然写挂了)
第二问就是求在一个时刻 (tim) 前树上路径上的关键点数。
由于它没有强制在线,离线显然更好做。我们把询问和修改都离线到时间上。
问题就转化成树上,单点加链查询,这个可以转化成子树加单点查的经典操作,不会可以看这里。
我们有树状数组维护区间加,单点查就行了。复杂度就是 (O(n log n)) 的,常数比较小。
代码
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
#define pb push_back
using namespace std;
template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
}
void File() {
#ifdef zjp_shadow
freopen ("2011.in", "r", stdin);
freopen ("2011.out", "w", stdout);
#endif
}
const int N = 2e5 + 1e3;
vector<int> G[N];
int ans1[N], ans2[N], n, rt;
int anc[N][21], dep[N], dfn[N], efn[N];
void Dfs_Init(int u) {
static int clk = 0;
if (u) dfn[u] = ++ clk;
for (int v : G[u])
dep[v] = dep[anc[v][0] = u] + 1, Dfs_Init(v);
efn[u] = clk;
}
#define lowbit(x) (x & -x)
template<int Maxn>
struct Fenwick_Tree {
int sumv[Maxn];
inline void Update(int pos, int uv) {
for (; pos <= n; pos += lowbit(pos)) sumv[pos] += uv;
}
inline void Update(int l, int r, int uv) {
Update(l, uv); Update(r + 1, - uv);
}
inline int Query(int pos) {
int res = 0;
for (; pos; pos -= lowbit(pos)) res += sumv[pos];
return res;
}
};
Fenwick_Tree<N> T;
struct Query {
int x, y, id;
};
int tot = 0, Up[N], lim;
vector<Query> Q[N];
inline int Lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
int gap = dep[x] - dep[y];
Fordown (i, lim, 0) if (gap >> i & 1) x = anc[x][i];
if (x == y) return x;
Fordown (i, lim, 0) if (anc[x][i] != anc[y][i])
x = anc[x][i], y = anc[y][i];
return anc[x][0];
}
int main () {
File();
n = read();
For (i, 1, n) G[read()].pb(i);
Dfs_Init(0);
lim = floor(log2(n));
For (j, 1, lim) For (i, 1, n)
anc[i][j] = anc[anc[i][j - 1]][j - 1];
int q = read();
For (tim, 1, q) {
int opt = read();
if (opt == 2) Up[tim] = read();
else {
int x = read(), y = read(), c = read() + 1, lca = Lca(x, y);
ans1[++ tot] = dep[x] + dep[y] - dep[lca] - dep[anc[lca][0]];
if (tim > c)
Q[tim - c].pb((Query) {x, y, tot});
}
}
For (i, 1, q) {
int u = Up[i];
if (u) T.Update(dfn[u], efn[u], 1);
for (auto cur : Q[i]) {
int x = cur.x, y = cur.y, id = cur.id, lca = Lca(x, y);
ans2[id] = T.Query(dfn[x]) + T.Query(dfn[y]) - T.Query(dfn[lca]) - T.Query(dfn[anc[lca][0]]);
}
}
For (i, 1, tot)
printf ("%d %d
", ans1[i], ans2[i]);
return 0;
}