2022“杭电杯”中国大学生算法设计超级联赛(1)
1011 - Random
签到题。
1012 - Alice and Bob
黑板上有 m 个不大于 n 的自然数,Alice 每次可以将黑板上剩余的数字分成两组,而 Bob 每次选择其中一组并擦除该组中的所有数字,然后将另一组中的所有数字减 1。
在游戏的任意时刻,只要黑板上出现了 0,则 Alice 获胜;若黑板上的所有数字均被擦除,则 Bob 获胜。
签到题,Alice 每次必然会把数字按值均分,设 a[i] 表示 i 的数量,则每轮操作后 a[i - 1] 至少增加 a[i] / 2,模拟 n 轮即可。
1009 - Laser
在二维平面上有 n 个点,问你平面上是否存在一个点,满足从这个点出发,沿上下左右和 45 度斜对角线共八个方向延伸的射线可以覆盖这 n 个点。
首先,如果所有点都在同一条水平/竖直/斜线上,那么可以直接输出 YES。
假设我们已经确定一个点在水平方向上(米字的一横),那么随便找一个不在这条直线上的点,用竖线和两条斜线会交在这条线的三个点上,只需要暴力判断这三个点作为中心点是否合法即可。
对于更普遍的情况,我们只需要每次将坐标旋转 45 度,将上面的过程重复 4 次,这样就可以讨论到所有情况了。
1002 - Dragon slayer
给你一副 n × m 的网格图,起点和终点均在某个网格的中心。在地图上有 k 面墙,每面墙都在网格线上。问你从起点到终点,最少要破坏多少面墙。
一开始想成“穿墙”了,写了个最短路结果 WA 了,赛后才意识到是一次性破坏一堵墙,又 k ≤ 15,直接暴力枚举破坏哪些墙,然后 BFS 验证即可。
2022“杭电杯”中国大学生算法设计超级联赛(2)
1002 - C++ to Python
签到题。
1009 - ShuanQ
已知 P × Q ≡ 1 (mod M),给你 P、Q,求质数 M。
签到题,枚举 PQ - 1 的质因子即可。
1007 - Snatch Groceries
给你 n 个区间,问你第一次重叠发生在第几个区间之后。
签到题,依题意模拟即可。
1012 - Luxury cruise ship
给你 7、31、365 三种面值的硬币,问你凑出 n 至少需要多少枚硬币。
由于 365 = 5 × 31 + 30 × 7,所以优先选 365 肯定是没问题的。对于 [0, 364) 的情况,直接 dp[i] = min(dp[i - 7], dp[i - 31]) + 1 预处理即可。
1001 - Static Query on Tree
树上静态查询,给你一棵有根树(树上的所有边均为儿子指向父亲的有向边),有 q 次查询。
每次查询给你 3 个点集 A、B、C,Alice 从点集 A 中的任意一个点出发,前往点集 C 中的任意一个点(前提是可达);Bob 从点集 B 中的任意一个点出发,前往点集 C 中的任意一个点(同上)。
显然,Alice 和 Bob 有可能在树上的某些点相遇,设这些点构成点集 D,求点集 D 的大小。
题意稍微有一点点绕,不过结合着样例还是可以理解。
说白了就是对于点集 A 中的所有点,将每个点到根的路径打上标记 1;对于点集 B 中的所有点,同理打上标记 2;对于点集 C 中的所有点,将每个点的子树打上标记 3。这样同时拥有标记 1、2、3 的点就构成了点集 D。
树上区间修改、子树修改、区间查询,这不就树剖 + 线段树么?考虑到修改是按顺序依次进行的,所以在线段树上用 3 个变量分别维护同时拥有标记 1、同时拥有标记 1&2、同时拥有标记 1&2&3 的点的数量即可。
代码如下:
#include<bits/stdc++.h>
const int MAXN=2e5+5;
struct edge
{
int to;
edge* next;
edge(int _to, edge* _next)
{
to=_to;
next=_next;
}
};
std::vector<edge*> head(MAXN);
void add(int x,int y)
{
head[x]=new edge(y,head[x]);
}
int fa[MAXN],siz[MAXN],son[MAXN];
void dfs1(int x,int f)
{
fa[x]=f,siz[x]=1;
for(edge* i=head[x];i;i=i->next)
{
if(i->to==f)
continue;
dfs1(i->to,x);
siz[x]+=siz[i->to];
if(siz[i->to]>siz[son[x]])
son[x]=i->to;
}
}
int idx,dfn[MAXN],top[MAXN];
void dfs2(int x,int f,int t)
{
dfn[x]=++idx,top[x]=t;
if(son[x])
dfs2(son[x],x,t);
for(edge* i=head[x];i;i=i->next)
{
if(i->to==f||i->to==son[x])
continue;
dfs2(i->to,x,i->to);
}
}
struct tree
{
struct node
{
int l,r;
int cnt[3];
int tag[3];
node(int _l=0,int _r=0)
{
l=_l,r=_r;
cnt[0]=cnt[1]=cnt[2]=0;
tag[0]=tag[1]=tag[2]=-1;
}
void pushdown(node& lc,node& rc)
{
for(int i=0;i<3;i++)
{
if(tag[i]==-1)
continue;
if(!i)
{
lc.cnt[i]=tag[i]*(lc.r-lc.l+1);
rc.cnt[i]=tag[i]*(rc.r-rc.l+1);
}
else
{
lc.cnt[i]=tag[i]*lc.cnt[i-1];
rc.cnt[i]=tag[i]*rc.cnt[i-1];
}
lc.tag[i]=tag[i];
rc.tag[i]=tag[i];
tag[i]=-1;
}
}
void pushup(const node& lc,const node& rc)
{
for(int i=0;i<3;i++)
cnt[i]=lc.cnt[i]+rc.cnt[i];
}
}a[MAXN<<2];
void build(int now,int l,int r)
{
a[now]=node(l,r);
if(l==r)
return;
int mid=l+r>>1;
build(now<<1,l,mid);
build(now<<1|1,mid+1,r);
}
void modify(int now,int l,int r,int k,int v)
{
if(a[now].l>r||a[now].r<l)
return;
if(l<=a[now].l&&a[now].r<=r)
{
if(!k)
a[now].cnt[k]=v*(a[now].r-a[now].l+1);
else
a[now].cnt[k]=v*a[now].cnt[k-1];
a[now].tag[k]=v;
return;
}
a[now].pushdown(a[now<<1],a[now<<1|1]);
modify(now<<1,l,r,k,v);
modify(now<<1|1,l,r,k,v);
a[now].pushup(a[now<<1],a[now<<1|1]);
}
int query(int now,int l,int r)
{
if(a[now].l>r||a[now].r<l)
return 0;
if(l<=a[now].l&&a[now].r<=r)
return a[now].cnt[2];
a[now].pushdown(a[now<<1],a[now<<1|1]);
return query(now<<1,l,r)+query(now<<1|1,l,r);
}
}t;
void modify1(int x,int k)
{
while(top[x]!=top[1])
{
t.modify(1,dfn[top[x]],dfn[x],k,1);
x=fa[top[x]];
}
t.modify(1,dfn[1],dfn[x],k,1);
}
void modify2(int x,int k)
{
t.modify(1,dfn[x],dfn[x]+siz[x]-1,k,1);
}
int query(int n)
{
return t.query(1,1,n);
}
void clear(int n)
{
t.modify(1,1,n,0,0);
t.modify(1,1,n,1,0);
t.modify(1,1,n,2,0);
}
int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-')
f=-1;
c=getchar();
}
while('0'<=c&&c<='9')
{
x=(x<<3)+(x<<1)+(c^'0');
c=getchar();
}
return x*f;
}
void solve()
{
int n=read(),q=read();
t.build(1,1,n);
for(int i=2;i<=n;i++)
{
int r=read();
add(i,r);
add(r,i);
}
dfs1(1,0);
dfs2(1,0,1);
while(q--)
{
int a=read(),b=read(),c=read();
while(a--)
modify1(read(),0);
while(b--)
modify1(read(),1);
while(c--)
modify2(read(),2);
std::cout<<query(n)<<'\n';
clear(n);
}
for(int i=1;i<=n;i++)
son[i]=0;
idx=0;
}
int main()
{
int t=read();
while(t--)
solve();
return 0;
}
2022“杭电杯”中国大学生算法设计超级联赛(3)
1003 - Cyber Language
签到题。
1009 - Package Delivery
有 n 个包裹依次到达邮局,第 i 个包裹在第 l[i] 天到达,最晚要在第 r[i] 天取回。你每次去邮局最多可以取 k 个包裹,问你最少要去多少次邮局,才能取回你的 n 个包裹。
显然我们只会在 ddl 取包裹,在 ddl 那天去一次邮局,为了尽量延后下一次取快递的日期,我们把 r 最小的那 k 个包裹取走即可(因为这 k 个马上就到 ddl 了),重复上述过程直到取完所有包裹。
1012 - Two Permutations
给你两个排列 P、Q,问你有多少种方案可以把它们归并成给定的序列 S。
考虑 dp,设 dp[i][j] 表示 P 的前 i 项匹配上了 S,且 P[i] 匹配 S 中数字 P[i] 第 j 次出现的位置时,有多少种合法的方案。转移时枚举 P[i + 1] 匹配哪个位置,那么 P[i] 匹配的位置中间的那段连续子串需要完全匹配 Q 中对应的子串,使用字符串 Hash 进行 O(1) 判断即可。
2022“杭电杯”中国大学生算法设计超级联赛(4)
1004 - Link with Equilateral Triangle
签到题。
1006 - BIT Subway
签到题,依题意模拟即可。
1007 - Climb Stairs
你来到了一个新的关卡,等待你的是一栋高为 n 的大楼,每一层有一个怪物,第 i 层的怪物的血量为 a[i],你从地面(第 0 层)出发,有一个初始攻击力 a[0]。
当你位于第 i 层时,你可以前往第 [max(i -1, 0), min(i + k, n)] 层,但你不能前往怪物血量大于你攻击力的那一层(否则你会死),也不能前往没有怪物的一层(曾经来过)。每当你杀死所在层的怪物,你可以吸收它的血量并加到你的攻击力中。
问你能否通关,即杀死所有怪物。
显然我们每次要从当前位置 l,找到一个右端点 r,使得可以按照 r、r - 1、r - 2、……、l + 1 的顺序击杀这一段的怪物。不难看出 r 更小的时候只会更优,不会更劣。
直接暴力维护当前想要到达的右端点(满足 l + k <= r),用线段树维护一下 [l, r] 的“攻击力 - 血量”。一旦扩展到 r 位置,就将 [l, r - 1] 区间加 a[r](代表吸收了第 r 层的怪物的血量),[r, r] 单点加 a[0] - a[r](代表需要击杀第 r 层的怪物)。
找到最小的满足要求的 r(即 [l, r] 区间最小值大于等于0,代表可以击败这一段的怪物了)之后,就可以更新 l 和 a[0] 了(代表击杀这一段的怪物)。如果在合法的右端点范围内找不到这样的 r,则输出 NO 即可。
1011 - Link is as bear
给你一个序列,每次操作你可以任选一个区间,求出这个区间的异或和 x,并把这个区间的每个数都变成 x。问你把整个序列变成同一个数后,这个数最大是多少。
注意,这个序列至少存在一对相同的数字。
手玩几组数据后,发现问题完全等价于给定 n 个数,从中选一些数使得这些数的异或和最大(证明过程可以看官方题解)。
而这是线性基的模板题,抄一个板子即可。
1002 - Link with Running
给你一幅 n 个点,m 条边的有向图,你要从 1 前往 n。每条边有两个边权 e 和 p,e 是代价,p 是价值。问在保证总代价最小的前提下,最小总代价和最大总价值分别是多少。
图论大杂烩题,整体思路是在最短路图上跑最长路。
1. 先用 dijkstra 在原图上跑出最短路图来,并求出第一个答案(最短路图的边权为 p)。
2. 注意到 e 可能为0,因此跑出来的最短路图不一定是个DAG,需要用 tarjan 将强连通分量缩起来。
3. 获得了 DAG 后,只需要在 DAG 上求最长路即可得到第二个答案。
2022“杭电杯”中国大学生算法设计超级联赛(5)
1012 - Buy Figurines
有 n 个人在 m 个窗口前排队买东西,第 i 个人的到达时间为 a[i],并且花费时间 s[i] 买东西(保证每个人的到达时间不同)。当一个人到达商店时,他将选择排队人数最少的队列,若存在多个人数最少的队列,则他选择编号最小的一个。问最后一个人什么时候离开。
队伍只有在有人到达和有人离开时才会发生变动,所以我们要在这些时刻维护这 m 个队伍的信息,暴力维护是 O(nm) 的,考虑优化。
首先队伍发生变动的时刻应该是没法优化的,这 O(n) 个时刻是必须要进行模拟的,所以考虑使用某种数据结构加速查询队伍和修改队伍的过程。
我们需要一种数据结构来帮我们完成以下两种操作:
1. 查询区间 [1, m] 最小值位置(有人到达时)。
2. 单点修改(有人到达和有人离开时)。
不难发现线段树和 std::set 都能很好地支持这两种操作,任选一种实现即可。
1010 - Bragging Dice
签到题。
1003 - Slipper
给你一棵 n 个点,根为 1 的带边权的树。如果两个点 a、b 的深度差的绝对值为常数 k,则从 a 到 b 就只需花费 p 的代价。问从 u 到 v 的最小代价。
相当于在深度差为 k 的两层点之间各自连了一条权值为 p 的边,暴力去连无论是时间复杂度还是空间复杂度都是不可接受的。
将这棵树看成一座高楼,每层的点看成一个个员工,我们可以给这座大楼修很多座直达电梯,让每位员工去坐电梯,第 i 层的员工可以乘坐第 i 座直达电梯前往第 i + k 层或第 i - k 层,且代价是 p。
用图论的语言描述以上过程,即为:在每个深度 d 新建一个点 t[d](代表第 i 座直达电梯),对于树上每个深度为 d 的点 u[d](代表这一层的员工),在 u[d] 和 t[d] 之间连一条边权为 0 的无向边(代表员工可以去坐电梯),并在 t[d] 和 t[d - k](t[d + k]) 之间连一条边权为 p 的有向边(代表员工坐电梯的时候需要花费 p 的代价)。
注意到这样连边时间复杂度和空间复杂度都是 O(n) 的,可以接受。
最后在这幅新图上跑最短路即可。
2022“杭电杯”中国大学生算法设计超级联赛(6)
1009 - Map
签到题。
1007 - Shinobu loves trip
有 P 个国家,编号为 0 ~ P - 1。
有 n 个旅游计划,第 i 个旅游计划从第 s[i] 个国家出发,旅行 d[i] 天。旅游的规则是:若今天在第 i 个国家,则明天去第 a × i mod P 个国家。
有 q 次查询,每次查询给你一个国家编号 x,问你有多少个旅游计划会经过 x。
从 s 走 d 天会来到 ,因此判断一个点 x 是否会被一个计划 (s, d) 经过,只需要判断是否存在一个 使得 。
预处理所有的 (将其映射到 0 ~ max(d)),以及每个旅游计划的 s 的逆元,然后每次就可以 O(log) 查询了。
具体地说,每次先 O(1) 计算 ,再 O(log) 查 map,最后 O(1) 与 d 比较即可。
1006 - Maex
给你一棵树,你要给树上的每个点 i 赋点权 a[i],需保证点权均为自然数且两两不同。定义 b[i] = mex(sub(i)),其中 sub(i) 代表 i 的子树中的点的点权构成的集合,mex(A) 代表集合 A 中最小的未出现过的自然数。
求 的最大值。
考虑树上 dp,设 dp[i] 表示 i 的子树的 sum(b) 的最大值,siz[i] 表示 i 的子树大小。
考虑转移,首先 i 本身的 b[i] 一定可以取到 siz[i],而因为 a 必须两两不同,所以 0 只可能在 i 的某个子树 j 中,所以除了子树 j,其他子树结点的 b 一定都为0。
于是有状态转移方程 dp[i] = siz[i] + max(dp[j])。
1010 - Planar graph
给你一副平面图,这副平面图把整个平面划分成了若干个区域。问你最少要删去多少条边,才能使整个平面只有一个区域,输出字典序最小的一种方案。
显然是要把这副平面图变成一个森林,又因为要让删去的边字典序最小,所以我们按边序号递减顺序跑 Kruskal 即可。
2022“杭电杯”中国大学生算法设计超级联赛(7)
1004 - Black Magic
签到题。
1003 - Counting Stickmen
给你一棵树,问你这棵树有多少个形状是“火柴人”的连通块。
“火柴人”有一条长度为 1 的链作为头部,两条长度为 2 的链作为手臂,一条长度 1 的链为身体,两条长度为 1 的链作为腿部。如图中红色连通块所示。
简单的树上统计题,显然是要让你维护一些东西来方便统计,思考需要维护哪些东西。
首先,我们很难注意不到上图中的 3 号点,这个点足足连接了 4 条边,地位举足轻重,相当于“火柴人”的核心,所以我们可以枚举树上的每个点作为核心点,统计答案。
确定好核心点之后,我们将核心点相连的 4 个方向,简单称作“头”、“胳膊”、“胳膊”、“腿”,然后分以下 4 类进行讨论:
(1)“胳膊”在父亲方向,“头”、“胳膊”、“腿”在子树中。
(2)“腿”在父亲方向,“头”、“胳膊”、“胳膊”在子树中。
(3)“头”为父亲,“胳膊”、“胳膊”、“腿”在子树中。
(4)“头”、“胳膊”、“胳膊”、“腿”均在子树中。
于是很自然地想到要维护每个结点的度数 deg、儿子数 son、孙子数 grandson。
观察后发现情况(1)较为简单,所以我们首先从这里入手:
不妨假设 x 为核心点,我们首先从 x 的父亲周围选取一个点,作为 x 的一条手臂的远心端(近心端即就是 x 的父亲),这一步有 C(deg[fa] - 1, 1) 种方案(减 1 是去掉 x);紧接着我们从 x 的某个儿子 y 中选俩孙子出来,作为 x 的“腿”,这一步有 C(son[y], 2) 种方案;再然后我们从 x 的孙子里面挑一个点出来(记得不要从 y 的儿子中挑),作为 x 的另一条手臂的远心端,这一步有 C(grandson[x] - son[y], 1) 种方案;最后我们再从 x 的儿子里面选一个点作为“头”即可(记得不要选 y 和刚才那个孙子的父亲),这一步有 C(son[x] - 2, 1) 种方案。
综上所述,情况(1)的 ans 为 。
同理,我们可以分析情况(2)的 ans:
类似地,我们首先从 x 的父亲周围选取两个点,作为 x 的“腿”,这一步有 C(deg[fa] - 1, 2) 种方案;接着我们从 x 的孙子里面选俩孙子出来,作为 x 的两根手臂的远心端,这一步原本是有 C(grandson[x], 2) 种方案,但考虑到选出来的俩孙子有可能来自于同一个儿子,所以还得减掉 x 的全部 C(son[y], 2) 之和(记作 sum[x]),这玩意在 dfs 的时候顺便维护一下就行;最后我们再从 x 的儿子里面选一个点作为“头”即可(同样记得不要选刚才那俩孙子的父亲),这一步有 C(son[x] - 2, 1) 种方案。
综上所述,情况(2)的 ans 为 。
接下来我们分析情况(3)的 ans:
首先 x 得有父亲,这样“头”就有了;接着我们从 x 的某个儿子 y 中选俩孙子出来,作为 x 的“腿”,这一步有 C(son[y], 2) 种方案;最后我们从 x 的孙子里面选俩孙子出来(记得不要从 y 的儿子中挑),作为 x 的两根手臂的远心端,这一步原本是有 C(grandson[x] - son[x], 2) 种方案,但是同样由于选出来的俩孙子可能来自于同一个儿子,所以再减掉 sum[x] - C(son[x], 2) 即可。
综上所述,情况(3)的 ans 为 。
情况(4)的分析略,ans 为 。
代码如下:
#include <bits/stdc++.h>
#define int long long
const int MAXN = 5e5 + 5;
const int mod = 998244353;
const int inv2 = 499122177;
int read()
{
int x = 0, f = 1;
char c = getchar();
while (c < '0' or c > '9')
{
if (c == '-')
f = -1;
c = getchar();
}
while ('0' <= c and c <= '9')
{
x = (x << 3) + (x << 1) + (c ^ '0');
c = getchar();
}
return x * f;
}
std::vector<int> g[MAXN];
int deg[MAXN];
int son[MAXN];
int grandson[MAXN];
int sum[MAXN];
int ans;
int C(int n, int m)
{
if (m == 0)
return n >= 0 ? 1 : 0;
else if (m == 1)
return n >= 1 ? n : 0;
else
return n >= 2 ? n * (n - 1) % mod * inv2 % mod : 0;
}
void dfs1(int x, int f)
{
son[x] = 0;
grandson[x] = 0;
sum[x] = 0;
for (int& i : g[x])
{
if (i == f)
continue;
dfs1(i, x);
son[x]++;
grandson[x] += son[i];
sum[x] = (sum[x] + C(son[i], 2)) % mod;
}
deg[x] = f ? son[x] + 1 : son[x];
}
void dfs2(int x, int f)
{
for (int& i : g[x])
{
if (i == f)
continue;
dfs2(i, x);
ans = (ans + C(son[i], 2) * (C(grandson[x] - son[i], 2) - (sum[x] - C(son[i], 2)) + mod) % mod * C(son[x] - 3, 1) % mod) % mod;
ans = (ans + C(deg[f] - 1, 0) * C(son[i], 2) % mod * (C(grandson[x] - son[i], 2) - (sum[x] - C(son[i], 2)) + mod) % mod) % mod;
ans = (ans + C(deg[f] - 1, 1) * C(son[i], 2) % mod * C(grandson[x] - son[i], 1) % mod * C(son[x] - 2, 1) % mod) % mod;
}
ans = (ans + C(deg[f] - 1, 2) * (C(grandson[x], 2) - sum[x] + mod) % mod * C(son[x] - 2, 1) % mod) % mod;
}
void solve()
{
int n = read();
for (int i = 1; i <= n; i++)
g[i].clear();
for (int i = 1; i < n; i++)
{
int a = read(), b = read();
g[a].push_back(b);
g[b].push_back(a);
}
dfs1(1, 0);
ans = 0;
dfs2(1, 0);
std::cout << ans << '\n';
}
signed main()
{
int t = read();
while (t--)
solve();
return 0;
}
PS:比赛时死活没想到情况(4),然后就凉凉了……
2022“杭电杯”中国大学生算法设计超级联赛(8)
1004 - Quel'Thalas
签到题。
1011 - Stormwind
一个 n × m 的长方形,可以沿水平或竖直方向画若干条线,每条线的两端点都在长方形的边界上。要求这些线划分出的每个小长方形面积都 >= k,求最多可以画几条线。
签到题,枚举小长方形的某个边长的最小值,由此可以求出另一个边长允许的最小值,然后求出两个方向分别最多能画几条线。
1001 - Theramore
一个 01 序列,可以任意翻转奇数长度的区间,求能达到的最小字典序。
只需使用长度为 3 的翻转,那么位置奇偶性相同的位置可以随便换。对奇数位置和偶数位置,分别把 0 放到前面,1 放到后面即可。
1008 - Orgrimmar
无向图的分离集是一个点集,如果我们只保留这些点之间的边,则集合中的每个点最多连接到一条边。求一棵树的最大分离集的大小。
简单的树上 dp,设 dp[0][x]、dp[1][x]、dp[2][x] 分别表示只考虑 x 的子树,x 未选、x 选了且度数为 0、x 选了且度数为 1 且子树是一个分离集,选择点数的最大值。
则有以下的状态转移方程:
,其中 y 是 x 的儿子(下同)。
,度数为 0 所以只能从 dp[0][y] 转移过来。
,枚举选哪个儿子即可。
2022“杭电杯”中国大学生算法设计超级联赛(9)
1010 - Sum Plus Product
签到题,输出 即可。
PS:比赛时好像做复杂了,写了个 NTT 过的,sto 队友 orz。
1008 - Shortest Path in GCD Path
给你一幅 n 个点的完全图,两个点之间的距离为它们的 gcd,q 次询问两个点之间的最短路以及方案数。
首先最短路长度不超过 2,因为对任意 (x, y),沿路径 x → 1 → y 即可得到一条长度为 2 的路径。最短路长度为 1 当且仅当 gcd(x, y) = 1,否则等价于询问 [1,n] 中满足 gcd(x, z) = 1 且 gcd(z, y) = 1 的 z 的数量。质因子分解 x, y 后用容斥可以解决。
值得注意的是,当 gcd(x, y) = 2 时,直接 x → y 也是一条长度为 2 的路径。
1003 - Fast Bubble Sort
给你一个长度为 n 的数组 A,令 B(A) 表示对 A 进行一轮冒泡排序循环之后得到的数组,令 num(A) 表示从 A 到 B(A) 最少需要区间循环移位的次数。
给你一个长度为 n 的排列 P 以及 q 组询问,每组询问给你 l, r,求 num(P[l, r])。
实际上是求区间的局部最大值的数量,但是如果存在连续一段均为局部最大值,则只取最后一个加入答案。设 L[x] 表示 x 左边第一个比 x 大的数的位置,R[x] 表示 x 右边第一个比 x 大的数的位置,则可以将上述要求表述为:
1. L[x] < l,x 左边第一个比 x 大的数在区间以外,这表明 x 确实是区间的局部最大值。
2. R[x] ≠ x + 1,x 右边第一个比 x 大的数与 x 不相邻,这表明 x 是连续一段局部最大值的最后一个。
于是将问题转化成了求区间满足以上要求的点的个数。又考虑到这是一个排列,所以用主席树可以很轻松地实现。
具体地说,我们将 R[x] = x + 1 的点的 L 设为 INF,这样就消去了第二个限制,变成了区间求 L[x] < l 的 x 的数量。而这用可持久化值域线段树可以很方便地实现,按照前缀和的思想,第 r 棵线段树的 ans 减去第 l - 1 棵线段树的 ans 即为区间 [l, r] 的 ans。
PS:比赛时把 l - 1 写成 l 导致痛失此题,赛后十几分钟就过了。
2022“杭电杯”中国大学生算法设计超级联赛(10)
1007 - Even Tree Split
给你一棵无根树,保证点数是偶数。你需要删除一些边(至少删 1 条),使得每个连通块的点数均为偶数。试求合法的方案数。
签到题,直接树上 dp 即可:
,其中 y 是 x 的儿子,siz[y] 是以 y 为根的子树的大小,中括号为 bool 表达式。
1003 - Wavy Tree
定义波动序列为:对于任意 ,有 或 。
给你一个序列 b,你要将 b 变为波动序列。为此,你每花费 1 枚硬币,可以使 b 中的一个元素增加或减少 1,求最小总代价。
签到题,直接贪心计算序列先增和序列先减的 ans,然后取 min 即可。
1004 - Average Replacement
有 n 个人,m 对朋友关系。首先,每个人在自己的帽子上写一个整数。他们计划多次玩以下游戏:每个人都用他和他的朋友们帽子上的数字的平均值,替换自己帽子上的数字。
也就是说,如果在一轮游戏前,某个人帽子上的数字为 ,他有 k 个朋友,第 i 个朋友帽子上的数字为 ,那么在这轮游戏后,他帽子上的数字就将变为 。
显然,如果他们玩无限轮游戏,那么每个人帽子上的数字都将收敛到某个值。给你每个人帽子上的初始数字,你的任务是计算这些值。
手玩几组数据后可以发现:对于每个连通块,有 ,其中 deg[i] 是 i 的朋友个数,siz[i] 是 i 所在的连通块的大小。
1009 - Painting Game
有 n 个连续的格子,初始均为白色。每次操作可以将一个格子涂黑,但是要保证任意两个黑色格子不相邻。Alice 和 Bob 轮流操作,不能操作时游戏结束。Alice 希望游戏结束时黑色格子尽量的少,Bob 希望游戏结束时黑色格子尽量的多。
给定 n 和先手,求游戏结束时黑色格子数量。
Alice 的一种最优策略是:选某个连续段的左数第二个格子涂黑,Bob 的一种最优策略是:选某个连续段的左数第三个格子涂黑。
设 f(n)、g(n) 分别为 Alice、Bob 面对长度为 n 的空纸带时的答案,则有:f(n) = g(n - 3) + 1, g(n) = f(n - 4) + 2(n ≥ 7)。
注意到事实上 f(n) = f(n - 7) + 3, g(n) = g(n - 7) + 3,因而可以 O(1) 计算答案。
1001 - Winner Prediction
有 n 名选手参与比赛,每场比赛都是两名选手之间的比赛,且没有平局。整场比赛的冠军是赢得比赛最多的选手,如果有多个选手赢得比赛最多,则他们都是冠军。
给你 m1 + m2 场比赛,其中 m1 场比赛已经结束,另 m2 场比赛还没开始。请问 1 号选手能否成为冠军?
先让 1 号选手赢下所有和他有关的比赛,设此时选手 i 赢了a[i] 场比赛。若此时存在某个 a[i] > a[1],则 1 号选手不可能成为冠军;否则,选手 i 最多还能赢 b[i] = a[1] - a[i] 场比赛。
考虑建一张网络流图:每场未进行的比赛在图中用一个点表示,源点向它连容量为 1 的边,它向它的两个参赛选手的对应点各自连容量为 1 的边,选手 i 的对应点向汇点连容量为 b[i] 的边。
计算该图的最大流,若源点出发的边满流则 ans 为 YES,否则为 NO。