2022“杭电杯”中国大学生算法设计超级联赛训练日志

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 天会来到 (s\cdot a^d)%P,因此判断一个点 x 是否会被一个计划 (s, d) 经过,只需要判断是否存在一个 0\leq k\leq d 使得 (s\cdot a^k)%P=x。 

        预处理所有的 a^k (将其映射到 0 ~ max(d)),以及每个旅游计划的 s 的逆元,然后每次就可以 O(log) 查询了。

        具体地说,每次先 O(1) 计算 x\cdot s^{-1}(mod P),再 O(log) 查 map,最后 O(1) 与 d 比较即可。

1006 - Maex

        给你一棵树,你要给树上的每个点 i 赋点权 a[i],需保证点权均为自然数且两两不同。定义 b[i] = mex(sub(i)),其中 sub(i) 代表 i 的子树中的点的点权构成的集合,mex(A) 代表集合 A 中最小的未出现过的自然数。

        求 \sum_{i=1}^n b_i 的最大值。

        考虑树上 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 为 \sum C_{deg_{fa}-1}^1\times C_{son_y}^2\times C_{grandson_x-son_y}^1\times C_{son_x-2}^1

        同理,我们可以分析情况(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 为 C_{deg_{fa}-1}^2\times (C_{grandson_x}^2-sum_x)\times C_{son_x-2}^1

        接下来我们分析情况(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 为 \sum C_{deg_{fa}-1}^0\times C_{son_y}^2\times (C_{grandson_x-son_y}^2-(sum_x-C_{son_x}^2))

        情况(4)的分析略,ans 为 \sum C_{son_y}^2\times (C_{grandson_x-son_y}^2-(sum_x-C_{son_x}^2))\times C_{son_x-3}^1

        代码如下:

#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 且子树是一个分离集,选择点数的最大值。

        则有以下的状态转移方程:

        dp[0][x]=\sum max(dp[0][y],dp[1][y],dp[2][y]),其中 y 是 x 的儿子(下同)。

        dp[1][x]=\sum dp[0][y],度数为 0 所以只能从 dp[0][y] 转移过来。

        dp[2][x]=dp[1][x]+max(dp[1][y]-dp[0][y]),枚举选哪个儿子即可。

2022“杭电杯”中国大学生算法设计超级联赛(9)

1010 - Sum Plus Product

        签到题,输出 \prod_{i=1}^n(a_i+1)-1 即可。

        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 即可:

        dp[x]=\prod dp[y]*(1+[siz[y]%2=0]),其中 y 是 x 的儿子,siz[y] 是以 y 为根的子树的大小,中括号为 bool 表达式。

1003 - Wavy Tree

        定义波动序列为:对于任意 i\in (1,n),有 a_i>max(a_{i-1},a_{i+1}) 或 a_i<min(a_{i-1},a_{i+1})

        给你一个序列 b,你要将 b 变为波动序列。为此,你每花费 1 枚硬币,可以使 b 中的一个元素增加或减少 1,求最小总代价。

        签到题,直接贪心计算序列先增和序列先减的 ans,然后取 min 即可。

1004 - Average Replacement

        有 n 个人,m 对朋友关系。首先,每个人在自己的帽子上写一个整数。他们计划多次玩以下游戏:每个人都用他和他的朋友们帽子上的数字的平均值,替换自己帽子上的数字。

        也就是说,如果在一轮游戏前,某个人帽子上的数字为 a_0,他有 k 个朋友,第 i 个朋友帽子上的数字为 a_i,那么在这轮游戏后,他帽子上的数字就将变为 \frac{\sum_{i=0}^ka_i}{k+1}

        显然,如果他们玩无限轮游戏,那么每个人帽子上的数字都将收敛到某个值。给你每个人帽子上的初始数字,你的任务是计算这些值。

        手玩几组数据后可以发现:对于每个连通块,有 ans=\frac{\sum a_i(deg_i+1)}{siz_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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值