【总结】JOISC2022
【总结】JOISC2022#
Day1#
T1:给定一棵树,有 个人,每个人从 出发要到 ,每次可以指定一个人走一条边。问是否存在一种方案让每个人都到 ,且满足任何两个人不同时出现在同一个节点,且每个人不走重复路径
分析一下,对于一个人的操作一定是连续的。如果我们先移动 ,再移动 ,然后移动 ,说明 挡住了 ,而 一定不会挡住 。那么我们先移动 ,再移动 一定更优。
所以我们要求一个顺序 ,使得按顺序依次将每个人移动到终点。那么对于两个人 ,如果 在 的路径上,则 必须在 前面,如果 在 的路径上,则 必须在 后面。
经典拓扑序建图,倍增优化建图即可,时空复杂度 。
#define N 120005
#define M 4400005
int n, m, s[N], t[N], d[N], in[M], f[N][17], u[N][17], v[N][17], idx, T, dfn[N], tot, sz[N];
vector<int>e[N], w[M]; queue<int>q;
void dfs(int x,int fa){
d[x] = d[f[x][0] = fa] + 1, dfn[x] = ++tot, sz[x] = 1;
u[x][0] = ++idx, v[x][0] = ++idx;
rp(i, T){
f[x][i] = f[f[x][i - 1]][i - 1];
u[x][i] = ++idx;
w[u[x][i - 1]].pb(idx); if(f[x][i - 1])w[u[f[x][i - 1]][i - 1]].pb(idx);
v[x][i] = ++idx;
w[idx].pb(v[x][i - 1]); if(f[x][i - 1])w[idx].pb(v[f[x][i - 1]][i - 1]);
}
go(y, e[x])if(y != fa)dfs(y, x), sz[x] += sz[y];
}
void ins(int id,int x,int y){
if(dfn[x] <= dfn[y] && dfn[x] + sz[x] > dfn[y]){
pre(i, T, 0)if(d[f[y][i]] >= d[x])w[u[y][i]].pb(id), y = f[y][i];
}
else{
x = f[x][0];
if(d[x] < d[y])swap(x, y);
pre(i, T, 0)if(d[f[x][i]] >= d[y])w[u[x][i]].pb(id), x = f[x][i];
if(x == y)w[u[x][0]].pb(id);
else{
pre(i, T, 0)if(f[x][i] != f[y][i])
w[u[x][i]].pb(id), w[u[y][i]].pb(id), x = f[x][i], y = f[y][i];
w[u[x][0]].pb(id), w[u[y][1]].pb(id);
}
}
}
void ins_(int id,int x,int y){
if(dfn[y] <= dfn[x] && dfn[y] + sz[y] > dfn[x]){
pre(i, T, 0)if(d[f[x][i]] >= d[y])w[id].pb(v[x][i]), x = f[x][i];
}
else{
y = f[y][0];
if(d[x] < d[y])swap(x, y);
pre(i, T, 0)if(d[f[x][i]] >= d[y])w[id].pb(v[x][i]), x = f[x][i];
if(x == y)w[id].pb(v[x][0]);
else{
pre(i, T, 0)if(f[x][i] != f[y][i])
w[id].pb(v[x][i]), w[id].pb(v[y][i]), x = f[x][i], y = f[y][i];
w[id].pb(v[x][0]), w[id].pb(v[y][1]);
}
}
}
void solve(){
read(n), T = log2(n), tot = 0;
rp(i, n)e[i].clear();
rp(i, idx)w[i].clear(), in[i] = 0;
rp(i, n - 1){
int x, y; read(x, y);
e[x].pb(y), e[y].pb(x);
}read(m), idx = m;
dfs(1, 0);
rp(i, m){
int x, y; read(x, y);
w[i].pb(u[x][0]), w[v[y][0]].pb(i);
ins(i, x, y), ins_(i, x, y);
}
int sz = 0;
rp(i, idx)go(x, w[i])in[x]++;
rp(i, idx)if(!in[i])q.push(i);
while(!q.empty()){
int x = q.front(); q.pop(), sz += x <= m;
go(y, w[x]){
in[y]--; if(!in[y])q.push(y);
}
}if(sz == m)puts("Yes"); else puts("No");
}
int main() {
int T; read(T);
while(T--)solve();
return 0;
}
T2:给定 的网格图,第 行权值为 ,第 列权值为 ,每次只能向右或者向下移动,求 的最短路。
对于 Sub1 直接 跑 DP即可。
对于 Sub2,对于 ,如果前后都有数 ,那么 一定用不上。所以优化后图的规模是 ,可以通过。
对于 Sub3,我们需要观察一些性质。
如果我们只经过一次转弯从 ,有两种路可以选择,分别是 和 。
这两条路对应的大小分别是 和 。
我们比较并化简一下可以得到 时选择第一条路更优,否则选择第二条路。
想必到这里已经很清楚了,我们的答案就是进行若干次这样的选择。而上面的式子显然时比较斜率,斜率越小越好。我们先对 求出下凸包,不在凸包上的一定存些斜率更小的两个点可以替代,然后合并两个凸包即可。
#define N 100005
int n, m, p[N], q[N], l, r; LL a[N], b[N];
int main() {
read(n, m);
rp(i, n)read(a[i]);
rp(i, m)read(b[i]);
p[l = 1] = q[r = 1] = 1;
rep(i, 2, n){
while(l > 1 && (a[i] - a[p[l]]) * (p[l] - p[l - 1]) <= (a[p[l]] - a[p[l - 1]]) * (i - p[l]))l--;
p[++l] = i;
}
rep(i, 2, m){
while(r > 1 && (b[i] - b[q[r]]) * (q[r] - q[r - 1]) <= (b[q[r]] - b[q[r - 1]]) * (i - q[r]))r--;
q[++r] = i;
}
int x = 1, y = 1, s = 1, t = 1; LL ans = 0;
while(x < n || y < m){
if(s == l || (t != r && (a[p[s + 1]] - a[p[s]]) * (q[t + 1] - q[t]) > (b[q[t + 1]] - b[q[t]]) * (p[s + 1] - p[s])))
ans += (q[t + 1] - q[t]) * a[x], y = q[++t];
else ans += (p[s + 1] - p[s]) * b[y], x = p[++s];
}
printf("%lld\n", ans);
return 0;
}
T3:求有多少个长度为 的字符串满足 个条件,每个条件形如 表示删除字符 后剩下串大于等于删除 后剩下的串。
观察一下,我们令 表示 和 的大小关系,那么限制 表示区间 中的第一个不等号是 , 表示第一个不等号是 。
设计 DP,用 表示前 个字符满足所有 的限制条件,以 结尾的方案。前缀和优化转移即可,时间复杂度 。
#define N 500005
int n, m, f[N][26], up[N][26], dn[N][26];
vector<int>u[N], v[N];
multiset<int>s, t;
int main() {
read(n, m);
rp(i, m){
int x, y;
read(x, y);
if(x < y)v[x + 1].pb(x), v[y + 1].pb(-x);
else u[y + 1].pb(y), u[x + 1].pb(-y);
}int ans = 26;
rep(i, 2, n){
go(x, u[i])
if(x > 0)s.insert(x); else s.erase(s.find(-x));
go(x, v[i])
if(x > 0)t.insert(x); else t.erase(t.find(-x));
int sl = 2, sr = 2;
if(!s.empty())sl = *s.rbegin() + 1;
if(!t.empty())sr = *t.rbegin() + 1;
if(sl < i)rep(r, 0, 24)ad(f[i][r], dn[i - 1][r + 1]), su(f[i][r], dn[sl - 1][r + 1]);
if(sr < i)rp(r, 25)ad(f[i][r], up[i - 1][r - 1]), su(f[i][r], up[sr - 1][r - 1]);
if(s.empty())rep(r, 0, 25)ad(f[i][r], 25 - r);
if(t.empty())rep(r, 0, 25)ad(f[i][r], r);
rep(j, 0, 25)ad(ans, f[i][j]);
up[i][0] = f[i][0]; rp(j, 25)up[i][j] = (up[i][j - 1] + f[i][j]) % P;
dn[i][25] = f[i][25]; pre(j, 24, 0)dn[i][j] = (dn[i][j + 1] + f[i][j]) % P;
rep(j, 0, 25)ad(up[i][j], up[i - 1][j]), ad(dn[i][j], dn[i - 1][j]);
}cout << ans << endl;
return 0;
}
Day2#
T1:有三个操作,花费 的代价在当前串结尾加一个字符,花费 的代价将当前串剪切进剪切板(注意是剪切不是复制),花费 的代价在当前串结尾粘贴。
我们定义 表示区间 的答案,直接按题意 DP 的复杂度是 的。
具体转移有两种,,和枚举 ,用 表示这次剪切的内容,然后贪心在 中删去最多个不重复的子串 。
用字符串哈希可以快速判断。为减少状态,我们添加一个转移 。然后进行剪切操作当且仅当 。这样预处理 KMP,每次跳 next 指针可以保证一定是合法的剪切。对于两个相同的子串我们只用计算一次,每跳一次 next 必然存在两个相同的子串,所以最多转移 次。
问题在于快速求出区间 中最多能选多少个不重复的 ,可以预处理 表示和 相同的不重复的后面 个子串左端点。倍增一下即可。时间复杂度 。
#define N 2505
typedef unsigned long long ull;
int n, nxt[N][N], ste[12][N][N]; char s[N]; LL A, B, C, f[N][N]; ull h[N], pw[N];
#define g(l, r) (h[r] - h[l - 1] * pw[r - (l) + 1])
#define M 19989997
LL g[M + 500]; ull ky[M + 500];
pair<ull, int>b[N];
int main() {
read(n);
scanf("%s", s + 1);
rp(l, n){
int j = 0;
rep(r, l + 1, n){
while(j && s[r] != s[l + j])j = nxt[l][l + j - 1];
if(s[r] == s[l + j])j++;
nxt[l][r] = j;
}
}
pw[0] = 1; rp(i, n)h[i] = h[i - 1] * R + s[i], pw[i] = pw[i - 1] * R;
rp(i, n){
int T = 0;
rp(j, n - i + 1)b[++T] = mp(g(j, j + i - 1), j);
sort(b + 1, b + T + 1, [](Pr x, Pr y){return x.fi != y.fi ? x.fi < y.fi : x.se > y.se;});
int k = 1;
rp(j, T){
while(k < j && b[k].fi != b[j].fi)k++;
while(k < j && b[j].se + i <= b[k + 1].se)k++;
if(k < j && b[j].se + i <= b[k].se)ste[0][b[j].se][i] = b[k].se;
}
}
rp(k, 11)rp(j, n)rp(i, n - j + 1)ste[k][i][j] = ste[k - 1][ste[k - 1][i][j]][j];
read(A, B, C); int tem = 0;
rp(r, n)pr(l, r){
if(l == r){f[l][r] = A; continue;}
ull v = g(l, r), now = v % M;
while(ky[now] && ky[now] != v)now++;
if(ky[now]){f[l][r] = g[now]; continue;}
ky[now] = v;
LL cur = min(f[l + 1][r], f[l][r - 1]) + A;
int t = (r - l + 1) / 2, k = nxt[l][r];
while(k && k > t)k = nxt[l][l + k - 1];
for(; k; k = nxt[l][l + k - 1]){
int cnt = r - l + 1, sum = 2, p = l;
pre(i, 11, 0)if(ste[i][p][k] && ste[i][p][k] + k - 1 <= r - k)p = ste[i][p][k], sum += 1 << i;
cnt -= sum * k;
cmn(cur, f[l][l + k - 1] + B + sum * C + cnt * A);
}
assert(tem <= 100000000);
f[l][r] = g[now] = cur;
}
printf("%lld\n", f[1][n]);
return 0;
}
T2:通信题,给 Alice 一颗树,Alice 将树上的点重新标号,标号区间为 。Bob 现在得知两个点的标号,告诉 Alice 20bits 的信息,Alice 反馈给 Bob bits 的信息,Bob 要利用反馈的信息算出这两个点的距离,X 越小得分越高。
20 bits的信息显然不能直接表示两个点 ,所以考虑将树上的点分组,然后传组号。
不难想到树分块,令 ,每块大小 。最多 块正好 10 bits 可以表示一个组号。
知道组号后把两个块内子树和两个块之间的距离反馈回去。所以我们对树但 dfs 序重标号,然后将 dfs 时入栈和出栈操作作为 反馈回去。这样就可以还原出一棵树。
写的好能拿 分左右,满分待补。
T3:给定若干三元组 ,求三元组的三元组 满足 ,,,使 最大。
考虑按 排序,对于每个三元组 ,将 更小的三元组加入集合 。我们只用找到集合中最大的 且 的三元组即可。直接 set 维护即可,时间复杂度 。
#define N 150005
int n, ans = ~0, sa, sb;
struct node{
int a, b, c;
bool operator<(const node o)const{return c < o.c;}
}a[N];
set<Pr>s;
void ins(int x,int y){
if(x <= sa && y <= sb)return;
if(x < sa)sb = y;
else if(y < sb)sa = x;
while(true){
auto cur = s.lower_bound(mp(x + 1, 0));
if(cur == s.end())break;
if((*cur).se < y)cmx(sa, (*cur).fi), cmx(sb, y), s.erase(cur);
else break;
}
while(true){
auto cur = s.lower_bound(mp(x, 0));
if(cur == s.begin())break;
cur--;
if((*cur).se > y)cmx(sa, x), cmx(sb, (*cur).se), s.erase(cur);
else break;
}
s.insert(mp(x, y));
while(!s.empty()){
auto cur = s.begin();
if((*cur).fi <= sa && (*cur).se <= sb)s.erase(cur);
else break;
}
}
void calc(int l,int r){
rep(i, l, r)if(sa > a[i].a && sb > a[i].b)cmx(ans, sa + sb + a[i].c);
rep(i, l, r)ins(a[i].a, a[i].b);
}
int main() {
read(n);
rp(i, n)read(a[i].a, a[i].b, a[i].c);
sort(a + 1, a + n + 1);
for(int i = 1; i <= n;){
int j = i;
while(a[j].c == a[i].c)j++;
calc(i, j - 1), i = j;
}printf("%d\n", ans);
return 0;
}
Day3#
T1:通信题,Alice 要告诉 Bob 一个 的数,Alice 给出两个等长的 串 , 会被归并成一个新串并告诉 Bob,Bob 要还原出这个数,归并策略是任意的(不随机)。
这题确实非常难以下手,这个归并就相当于洗扑克的叠牌,洗了后原来的信息就没了。
由于数 ,意味着我们需要保留 个有效位。
对于 ,考虑牺牲一个串成全另一个串。首先我们让 串等价于 串,那么我们让 ,那么对于 的前缀和是 。
那么对于串 ,我们将每一位重复 遍。对于归并后的串求前缀和 ,显然 的 是唯一的,后面的 是 串带来的波动,而 则是有效信息,如果 增了说明该有效位是 ,否则是 。
所以需要 个 bits 能得到 分。满分待补。
T2: 给定一棵树,支持两个操作。
1 x y z
表示到点 的距离 的点都 ,2 x
表示查询点 的权值,。
定义 表示将点 子树中距离 的点都乘 。每次修改和询问最多和最多 个祖先有关。直接模拟复杂度是 ,差分一下可以做到 。
#define N 200005
int n, u[N], fa[N], w[N], t, P; vector<int>e[N];
void dfs(int x,int f){
fa[x] = f; go(y, e[x])if(y != f)dfs(y, x);
}
int f[41][N], c[10];
int main() {
read(n, P);
rp(i, n - 1){
int x, y; read(x, y);
e[x].pb(y), e[y].pb(x);
}
srand(time(0));
dfs(rand() % n + 1, 0);
rp(i, n){
read(u[i]);
rep(j, 0, 40)f[j][i] = 1;
}
int T; read(T);
while(T--){
int op, x, y; LL z; read(op, x);
if(1 == op){
read(y, z);
int d = y, r = x;
while(r && d >= 0){
if(fa[r] && d > 1)
f[d][r] = f[d][r] * z % P, f[d - 1][r] = f[d - 1][r] * z % P;
else rep(i, 0, d)f[i][r] = f[i][r] * z % P;
d--, r = fa[r];
}
}
else{
LL cur = u[x];
y = x; int d = 40;
while(y && d >= 0){
cur = cur * f[40 - d][y] % P, d--, y = fa[y];
}printf("%lld\n", cur);
}
}
return 0;
}
T3:
待补
Day4#
T1:交互题。 种颜色,每种颜色 个球。 个不同颜色的球可以组成一组。每次可以询问一个集合 中的球最多能组成多少组。现在要将所有球分成 组,最多询问 次,。
从左向右一次询问区间 ,第一个返回 的位置 一定是该颜色的第一次出现。
将 加入当前集合 ,然后从后向前扫,如果当前扫到位置 ,区间 加上 集合里的数不能组成一组,则把 加入 集合。
扫一个来回后 集合就是一个合法组。直接扫需要询问 次,显然是无法通过的。
我们可以优化一下,向右扫的过程我们可以直接二分答案。向左扫虽然也可以二分,但是需要二分的次数太多反而是负优化,我们类似循环展开每次向前跳 个位置即可。这样期望的询问次数 勉强可以通过。
但是如果所有球是有序的会被卡成 ,所以我们开始前对所有标号随机打乱,这样无论数据如何对于我们都是随机均匀的。
mt19937 rd(765234);
#define maxn 10005
int p[maxn], a[maxn], b[maxn], t;
vector<int>c;
int ask(int x){
vector<int>u = c;
rp(i, x)u.pb(p[a[i]]);
return Query(u);
}
void Solve(int n,int m){
rp(i, n * m)p[i] = i, a[i] = i;
shuffle(p + 1, p + n * m + 1, rd);
t = n * m;
rp(i, m){
if(i == m){
rp(j, t)c.pb(p[a[j]]);
Answer(c); return ;
}
int l = n, r = min(t, 4000), w = 0;
while(l <= r){
int mid = (l + r) >> 1;
if(ask(mid))w = mid, r = mid - 1;
else l = mid + 1;
}
c.pb(p[a[w]]), a[w--] = 0;
while(si(c) < n){
if(w < 5){
if(!ask(w - 1)){
c.pb(p[a[w]]), a[w] = 0;
}w--;
}
else{
if(ask(w - 3))w -= 3;
else{
if(!ask(w - 1)){
c.pb(p[a[w]]), a[w] = 0, w--;
}
else if(!ask(w - 2)){
c.pb(p[a[w - 1]]), a[w - 1] = 0, w -= 2;
}
else c.pb(p[a[w - 2]]), a[w - 2] = 0, w -= 3;
}
}
}
Answer(c), c.clear();
int T = 0;
rp(j, t)if(a[j])b[++T] = a[j];
t = T; rp(j, t)a[j] = b[j];
}
}
T2:给定一个序列,支持单点修改和区间查询。每个数可以吞并相邻的不大于它的数,变成两数之和。每次询问区间内有多少个数可以吞下整个区间。
最大数一定可以,我们以最大值为分界,如果左边之和大于最大值,则递归统计左边,对右边同理,类似于笛卡尔树。直接做是 可以得到 25 分。如果没有修改操作,我们可以从区间左端点每次跳到右边第一个比自己大的位置,一直到区间最大值,并统计答案,倍增可以优化至 ,可以得到 分。
其余待补。
T3:给定带权无向图,每次询问 ,表示如果所有边权变为 的最小生成树。
将所有边按照边权排序,求出前缀最小生成树和后缀生成树。对于一次询问 相当于合并前后两颗树得到新的最小生成树。
直接做时间复杂度 ,只有 分。
太大了考虑优化掉。
对于预处理的过程,从小到大每次加入一条边,如果成环则删除一条边。
如果没有删边,那么对于所有 ,都要选择当前的边。否则我们设删除的边为 ,当前边为 ,那么 一定是 对应树的路径最大的边,边权 的其他边一定不在路径上,所以当 时选择 ,否则选择 。
直接做是 ,由于每次是加一条边并删一条边,可以直接在树上找最大的边,复杂度 ,也可以用 LCT 维护做到 。
#define N 505
#define M 100005
int n, m, fa[N];
int get(int x){return fa[x] == x ? x : fa[x] = get(fa[x]);}
struct node{
int u, v, w;
bool operator<(const node o)const{return w < o.w;}
}e[M], u[N], v[N];
vector<node>a;
int main() {
read(n, m);
rp(i, m)read(e[i].u, e[i].v, e[i].w);
sort(e + 1, e + m + 1);
int t = 0;
rp(i, m){
rp(i, n)fa[i] = i;
fa[e[i].u] = e[i].v;
int p = 0, dl = 0;
v[++p] = e[i];
rp(i, t){
if(dl)v[++p] = u[i];
else{
int x = get(u[i].u), y = get(u[i].v);
if(x != y)fa[x] = y, v[++p] = u[i];
else dl = i;
}
}
if(!dl)a.pb(node{-1, e[i].w, 0}), a.pb(node{2, -2 * e[i].w, e[i].w});
else {
a.pb(node{-1, u[dl].w, (e[i].w + u[dl].w + 1) >> 1});
a.pb(node{-1, e[i].w, (e[i].w + u[dl].w + 1) >> 1});
a.pb(node{2, -2 * e[i].w, e[i].w});
}
t = p; rp(i, t)u[i] = v[i];
}
sort(a.begin(), a.end());
int j = 0, sz = si(a); LL l = 0, r = 0;
int T; read(T);
while(T--){
int x; read(x);
while(j < sz && a[j].w <= x)l += a[j].u, r += a[j].v, j++;
printf("%lld\n", l * x + r);
}
return 0;
}
作者:7KByte
出处:https://www.cnblogs.com/7KByte/p/16049744.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通