Educational Codeforces Round 109 div2 A~F题解

视频讲解:BV1MU4y1L7FB

A. Potion-making

题目大意

需要调配出精华浓度为 k % k\% k% 的药剂,每次可以添加 1 1 1 升水或 1 1 1 升精华,求最少需要添加几次。 k k k [ 1 , 100 ] [1,100] [1,100] 范围内的整数。

题解

调配出精华浓度为 k % k\% k% 的药剂,即水与精华的比例 a b = 100 − k k \frac{a}{b}=\frac{100-k}{k} ba=k100k 。易得当 a b \frac{a}{b} ba 为最简分数, a + b a+b a+b 最小。
a = 100 − k g c d ( k , 100 − k ) a=\frac{100-k}{gcd(k,100-k)} a=gcd(k,100k)100k b = k g c d ( k , 100 − k ) b=\frac{k}{gcd(k,100-k)} b=gcd(k,100k)k
特别的,当 k = 100 k=100 k=100 时, 应取 a = 1 , b = 0 a=1,b=0 a=1,b=0 。在部分求解最大公约数的代码写法里,可能会出现错误结果。

参考代码

#include<bits/stdc++.h>
using namespace std;
 
int gcd(int a,int b)
{
	return a%b?gcd(b,a%b):b;
}
 
int main()
{
	int T,k,g,ans;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&k);
		g=gcd(100-k,k);
		ans=k/g+(100-k)/g;
		printf("%d\n",ans);
	}
}

B. Permutation Sort

题目大意

给定一个 1 1 1 n n n 的排列 a [ ] a[] a[] ,每次操作可以选择一个子区间(但不能选择整个排列),随意调整该区间内数的顺序。
求最少需要操作几次,使得排列 a [ ] a[] a[] 变为递增序列。

题解

因为子区间可以随意选取,不妨假设每次都选取一个长度为 n − 1 n-1 n1 的区间,即选择区间 [ 1 , n − 1 ] [1,n-1] [1,n1] 或区间 [ 2 , n ] [2,n] [2,n]

接下来分几种情况讨论:

  1. 若原排列就是一个递增序列,则无需修改,共操作 0 0 0 次;
  2. a 1 = 1 a_1=1 a1=1 a n = n a_n=n an=n ,则只需要将区间 [ 2 , n ] [2,n] [2,n] 或区间 [ 1 , n − 1 ] [1,n-1] [1,n1] 排序即可,共操作 1 1 1 次;
  3. a 1 = n a_1=n a1=n a n = 1 a_n=1 an=1 ,则需要先操作 2 2 2 次,将 1 1 1 放到 a 1 a_1 a1 上(或将 n n n 放到 a n a_n an 上),再操作 1 1 1 次将剩余数排序,共操作 3 3 3 次;
  4. 对于其他情况,则操作一次将 1 1 1 放到 a 1 a_1 a1 上(或将 n n n 放到 a n a_n an 上),再操作 1 1 1 次将剩余数排序,共操作 2 2 2 次;

参考代码

#include<bits/stdc++.h>
using namespace std;
 
const int MAXN=60;
int a[MAXN];
 
int main()
{
	int T,n,flag,i;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		flag=1;
		for(i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			if(a[i]!=i)
				flag=0; 
		}
		if(flag)
			printf("0\n");
		else if(a[1]==1||a[n]==n)
			printf("1\n");
		else if(a[1]==n&&a[n]==1)
			printf("3\n");
		else
			printf("2\n");
	}
}

C. Robot Collisions

题目大意

n n n 个机器人在数轴上。数轴上坐标为 0 0 0 m m m 的位置为墙壁。
i i i 个机器人初始在整数坐标 x i ( 0 < i < m ) x_i(0< i < m) xi(0<i<m) 上,并朝左(朝向原点)或朝右(朝向无穷大)以每秒 1 1 1 个单位的速度前进。机器人初始所在位置不重复。
每当机器人抵达墙壁时,会立刻调转方向,以同样的速度沿相反方向前进。
每当两个机器人同时抵达同一整数坐标时,他们会发生碰撞并爆炸。如果在非整数坐标相遇,则不会爆炸。
求每个机器人是否会爆炸,若会则输出爆炸时刻,否则输出 − 1 -1 1

题解

首先注意到一点,就是只有在整数坐标相遇时才会爆炸。因此可以把所有机器人按照初始坐标的奇偶性分为两类,只有相同奇偶性坐标的机器人之间才有可能碰撞。

接下来考虑相同奇偶性的初始坐标的机器人之间的碰撞的问题。
如果不考虑两侧的墙,会发现这就是一个左右括号匹配的问题。

  • 括号匹配问题:给定一个由左右括号构成的字符串,求合法的最大左右括号匹配数。通常采用栈来解决。

朝右走的机器人代表左括号,朝左走的机器人代表右括号,求这个括号字符串直接的相互匹配关系。这个问题可以用栈解决。碰到左括号则将其放入栈中,碰到右括号则将其与栈顶的左括号匹配(没有则无配对)。

然后加入对墙壁的考虑。如果当前栈空,即当前机器人左侧没有其他机器人,或者左侧的机器人都两两碰撞消灭掉了(即括号均匹配成功),那么即使当前机器人是朝左走,也是可以加入栈中与其他朝左走的机器人进行匹配的。因为当前机器人朝左走到头后撞墙,会调转方向变为朝右走。即对于最左侧的机器人来说,不管其初始是朝左走还是朝右走,都可以变化为朝右走。这样就可以部分解决撞墙问题了。
若最左侧尚未匹配的机器人位于 x i x_i xi 坐标朝左走,我们可以将坐标 0 0 0 处的墙壁当作镜子,将其等价于位于 − x i -x_i xi 坐标朝右走。这样就能作为左括号加入栈中参与匹配。

如上从左到右遍历后,可能会出现栈中元素个数 ≥ 2 \geq2 2 的情况,即有至少 2 2 2 个机器人是朝右走的。那么我们可以不断取出栈顶的两个元素,即最右侧的两个机器人,他们必会在最右侧的机器人撞墙反向后,相互发生碰撞。
同样将坐标 m m m 处的墙当作镜子,位于 x i x_i xi 坐标的机器人向右走,等价于位于 2 m − x i 2m-x_i 2mxi 坐标的机器人向左走。
这样不断取出栈顶两个元素,计算他们的碰撞时刻,直到栈中的元素个数 ≤ 1 \leq 1 1,即只剩最多 1 1 1 个机器人时,结束计算。

参考代码

#include<bits/stdc++.h>
using namespace std;

const int MAXN=300300;
struct Robot
{
	int id,x;
	char dir;
	bool operator <(Robot r)
	{
		return x<r.x;
	}
}a[MAXN],stk[MAXN];

int m;
int ans[MAXN];
vector<Robot> v0,v1;

void cal(vector<Robot>& vec)
{
	int cnt=0,i;
	for(i=0;i<vec.size();i++)
	{
		if(cnt==0||vec[i].dir=='R')
		{
			if(vec[i].dir=='L')
				vec[i].x=-vec[i].x;
			stk[cnt++]=vec[i];
		}
		else
		{
			Robot pre=stk[--cnt];
			ans[vec[i].id]=ans[pre.id]=(vec[i].x-pre.x)/2;
		}
	}
	while(cnt>=2)
	{
		Robot r=stk[--cnt];
		Robot l=stk[--cnt];
		r.x=2*m-r.x;
		ans[r.id]=ans[l.id]=(r.x-l.x)/2;
	}
}

int main()
{
	int T,n,i;
	scanf("%d",&T);
	while(T--)
	{
		v0.clear();
		v1.clear();
		scanf("%d%d",&n,&m);
		for(i=1;i<=n;i++)
		{
			scanf("%d",&a[i].x);
			a[i].id=i;
			ans[i]=-1;
		}
		for(i=1;i<=n;i++)
			scanf(" %c",&a[i].dir);
		sort(a+1,a+n+1);
		for(i=1;i<=n;i++)
		{
			if(a[i].x&1)
				v1.push_back(a[i]);
			else
				v0.push_back(a[i]);
		}
		cal(v0);
		cal(v1);
		for(i=1;i<=n;i++)
			printf("%d ",ans[i]);
		puts("");
	}
}

D. Armchairs

题目大意

给定一个长度为 n n n 的仅由 0 0 0 1 1 1 构成的数组 a [ ] a[] a[],其中 1 1 1 的个数不超过 n 2 \frac{n}{2} 2n 个。
每次操作,可以选择一对的点对 < i , j > < i , j > <i,j> ,满足 a i ≠ a j a_i \neq a_j ai=aj ,然后交换 a i a_i ai a j a_j aj ,代价为 ∣ i − j ∣ |i-j| ij
求最少的代价,使得原数组中值为 1 1 1 的元素,均变为值为 0 0 0

题解

理解题意后,我们发现就是为每一个 1 1 1 寻找一个独一无二的 0 0 0
我们将所有的 0 0 0 构成一个集合,所有的 1 1 1 构成另一个集合。同一集合内的元素两两无连边,不同集合间的元素有一条权值为 ∣ i − j ∣ |i-j| ij 的连边( i i i 0 0 0 所在的位置, j j j 1 1 1 所在的位置)。
如果学过二分图的话,会发现这就是求二分图最大匹配情况下的最小总花费,即二分图最优匹配问题。对于这个问题,通常采用KM算法求解,但对于这题来说,KM算法复杂度超标,并且也不需要。

考虑如何配对更优。假设存在两个 0 0 0 ,分别在原数组的 x 1 , x 2 ( x 1 < x 2 ) x_1,x_2(x_1 < x_2) x1,x2(x1<x2) 位置上,另存在两个 1 1 1 ,分别左原数组的 y 1 , y 2 ( y 1 < y 2 ) y_1,y_2(y_1 < y_2) y1,y2(y1<y2) 上。易得以下不等式:
∣ x 1 − y 1 ∣ + ∣ x 2 − y 2 ∣ ≤ ∣ x 1 − y 2 ∣ + ∣ x 2 − y 1 ∣ |x_1-y_1|+|x_2-y_2| \leq |x_1-y_2|+|x_2-y_1| x1y1+x2y2x1y2+x2y1
也就是说,对于左边的 1 1 1 ,其配对左边的 0 0 0 更优;对于右边的 1 1 1 ,其配对右边的 0 0 0 更优;
那么对于数组中最后一个 1 1 1 ,与其配对的 0 0 0 之后的其他 0 0 0,必定是无法得到配对的。否则交换配对关系,会得到一个更优的结果。

更数学化的描述,定义原数组 a [ ] a[] a[] 中所有 0 0 0 元素的下标构成新数组 x [ ] x[] x[] ,满足 a x i = 0 a_{x_i}=0 axi=0
原数组 a [ ] a[] a[] 中所有 1 1 1 元素的下标构成新数组 y [ ] y[] y[] ,满足 a y i = 1 a_{y_i}=1 ayi=1
d p i , j dp_{i,j} dpi,j 表示前 i i i 0 0 0 与前 j j j 1 1 1 构成 j j j 组配对时的最小花费。考虑最后一对配对的 01 01 01,有以下两种可能:

  • y j y_j yj 位置上的 1 1 1 x i x_i xi 位置上的 0 0 0 配对,即 d p i , j = d p i − 1 , j − 1 + ∣ x i − y j ∣ dp_{i,j}=dp_{i-1,j-1}+|x_i-y_j| dpi,j=dpi1,j1+xiyj
  • y j y_j yj 位置上的 1 1 1 x i x_i xi 之前的某个 0 0 0 配对,也就是说末位还有多余的 0 0 0 未配对,即 d p i , j = d p i − 1 , j dp_{i,j}=dp_{i-1,j} dpi,j=dpi1,j

综上,则有动态转移式:
d p i , j = m i n ( d p i − 1 , j , d p i − 1 , j − 1 + ∣ x i − y j ∣ ) dp_{i,j}=min(dp_{i-1,j},dp_{i-1,j-1}+|x_i-y_j|) dpi,j=min(dpi1,j,dpi1,j1+xiyj)

实现时,可以利用滚动数组或调整 j j j 循环的顺序,压缩掉 d p dp dp 数组的 i i i 维度空间开销。

参考代码

#include<bits/stdc++.h>
using namespace std;

const int MAXN=5050;
int cnt0,cnt1;
int a[MAXN],dp[MAXN][MAXN],id0[MAXN],id1[MAXN];

int main()
{
	int n,i,j;
	scanf("%d",&n);
	cnt0=cnt1=0;
	for(i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		if(a[i])
			id1[cnt1++]=i;
		else
			id0[cnt0++]=i;
	}
	memset(dp,0x3f,sizeof(dp));
	dp[0][0]=0;
	for(i=1;i<=cnt0;i++)
	{
		dp[i][0]=0;
		for(j=1;j<=cnt1;j++)
			dp[i][j]=min(dp[i-1][j],dp[i-1][j-1]+abs(id0[i-1]-id1[j-1]));
	}
	printf("%d\n",dp[cnt0][cnt1]);
}

E. Assimilation IV

题目大意

n ( 1 ≤ n ≤ 20 ) n(1 \leq n \leq 20) n(1n20) 个城市和 m ( 1 ≤ m ≤ 5 ⋅ 1 0 4 ) m(1 \leq m \leq 5 \cdot 10^4) m(1m5104) 个点,第 i i i 座城市与第 j j j 个点的距离为 d i , j ( 1 ≤ d i , j ≤ n + 1 ) d_{i,j}(1 \leq d_{i,j} \leq n+1) di,j(1di,jn+1)
你可以在城市建造纪念碑。具有纪念碑的城市,会不断占领与其相邻的点。建造纪念碑后的第一回合会占领与该城市距离 1 1 1 以内的点,第二回合会占领距离 2 2 2 以内的点……第 n n n 回合会占领距离 n n n 以内的点。
n n n 个回合,每回合随机选择一个未被选择选择的城市建造纪念碑。求 n n n 回合后,被占领的点的数量期望。结果对 998244353 998244353 998244353 取模。

题解

由于每个点是否被占领是相互独立的。考虑计算每个节点被占领的概率,最终汇总到总和期望中。
若一个节点不会被占领,则对于任意 i ( 1 ≤ i ≤ n ) i(1 \leq i \leq n) i(1in) ,第 i i i 回合建造的城市与该节点的距离必须不小于 n − i + 2 n-i+2 ni+2
s u m d sum_{d} sumd 表示与该节点距离不小于 d d d 的城市数量,则 n n n 回合后该节点未被占领的概率 p p p 可以表示为
p = ∏ i = 1 n s u m n − i + 2 − i + 1 n − i + 1 p=\prod_{i=1}^{n}{\frac{sum_{n-i+2}-i+1}{n-i+1}} p=i=1nni+1sumni+2i+1
那么该节点会被占领的概率为 1 − p 1-p 1p ,求所有节点被占领概率的总和即是被占领数量的数学期望。

参考代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll mod=998244353;
const int MAXN=25;
const int MAXM=50050;
ll inv[MAXN],num[MAXM][MAXN];

ll powmod(ll x,ll p)
{
	ll ret=1;
	while(p)
	{
		if(p&1)
			ret=ret*x%mod;
		x=x*x%mod;
		p>>=1;
	}
	return ret;
}

int main()
{
	int n,m,d,i,j;
	ll p,legal,ans;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n;i++)
		inv[i]=powmod(i,mod-2);
	for(i=1;i<=n;i++)
		for(j=1;j<=m;j++)
		{
			scanf("%d",&d);
			num[j][d]++;
		}
	ans=0;
	for(j=1;j<=m;j++)
	{
		p=1;
		legal=0;
		for(i=1;i<=n;i++)
		{
			legal+=num[j][n+2-i];
			if(legal<=0)
			{
				p=0;
				break;
			}
			p=p*legal%mod*inv[n-i+1]%mod;
			legal--;
		}
		ans=((ans+1-p)%mod+mod)%mod;
	}
	printf("%lld\n",ans);
}

F. Goblins And Gnomes

题目大意

有一个由 n ( 2 ≤ n ≤ 50 ) n(2 \leq n \leq 50) n(2n50) 个点与 m ( 0 ≤ m ≤ n ( n − 1 ) 2 ) m(0 \leq m \leq \frac{n(n-1)}{2}) m(0m2n(n1)) 条有向边构成的城堡。城堡中无法从一个点出发后,再回到这个点。

城堡会遭受 k k k 波袭击,第 i i i 波袭击会有 i i i 只哥布林前来掠夺。哥布林会出现在一些点上,每个节点最多 1 1 1 只哥布林。然后哥布林们会沿着有向边移动,掠夺路径上的所有节点。
哥布林贪婪且狡猾,他们会选择自己的掠夺路线,以确保没有两只哥布林会经过同一个节点(否则后来的哥布林就搜刮不到东西了)。在所有可能的掠夺方案中,他们会选择总掠夺节点最多的方案。

如果所有节点都被掠夺,则防守失败。只要有一个节点未被掠夺,则防守成功并恢复城堡。在下一波袭击中,哥布林仍有可能掠夺上一波已被掠夺过的节点(因为节点上的财宝也恢复了)。

每波袭击前,你可以进行一些操作进行防御。操作数量没有限制,但操作数越多,防御这波袭击后所得的分数越少。如果在第 i i i 波袭击前进行了 t i t_i ti 次操作,则会获得 m a x ( 0 , x i − y i ∗ t i ) max(0,x_i-y_i*t_i) max(0,xiyiti),其中 x i , y i ( 1 ≤ x i , y i ≤ 1 0 9 ) x_i,y_i(1 \leq x_i,y_i \leq 10^9) xi,yi(1xi,yi109) 已知。
每次操作,可以删除某个节点的所有入边,或所有出边。删除操作永久有效,即之后的每波袭击中,这些边都会被删掉。

求成功防御 k k k 波袭击后,所能获得最大分值的方案。

题解

借鉴了cjy2003的代码。
首先考虑哥布林是如何进攻城堡的,即在第 i i i 波袭击中,派出 i i i 只哥布林的情况下,能否进攻成功。由于哥布林之间的路径不会相交,因此这是一个DAG的最小路径覆盖问题。

DAG最小路径覆盖:给定一张有向无环图,求最少用几条不相交的路径,才能覆盖所有节点。通常将其转化为二分图最大匹配问题进行求解。

因此利用匈牙利算法,求得最大匹配数 m t h mth mth ,若 i ≤ n − m t h i \leq n-mth inmth 则防守成功,反之防守失败。

遇到防守失败时,需要进行防御操作。由于数据量较小,我们可以枚举每个点,尝试删除该节点的出边或入边,再跑一遍二分图来判断。
注意到,删除一个节点的所有出边或入边,等价与在二分图中删除一个节点时,其最大匹配数最多减少 1 1 1 。因此进行一次防御操作后,最多使得成功袭击所需的哥布林数量 + 1 +1 +1 ,因此只需找到任意一个能使得最大匹配数减少的删点操作即可,不存在更优的情况。

由于防御操作是永久生效的,因此若之前进行的防御操作花费更小,即 y i y_i yi 更小,则应该把当前的防御操作都在之前更便宜的时刻完成。
另外,由于每轮所得分数为 m a x ( 0 , x i − y i ∗ t i ) max(0,x_i-y_i*t_i) max(0,xiyiti) ,即最小为 0 0 0 ,因此存在放弃当轮分数,直接将所有节点的所有入边出边都删掉的策略,需要进行对比判断。

参考代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN=110;
int n;
int mp[MAXN][MAXN],used[MAXN],match[MAXN];
int tmp[MAXN],vx[MAXN],vy[MAXN],no_out[MAXN],no_in[MAXN];
ll prex[MAXN];
vector<pair<int,int> > ansseq,curseq;

bool dfs(int x)
{
	for(int i=1;i<=n;i++)
	{
		if(mp[x][i]&&!used[i])
		{
			used[i]=1;
			if(!match[i]||dfs(match[i]))
			{
				match[i]=x;
				return true;
			}
		}
	}
	return false;
}

int maxMatch()
{
	int ret=0;
	memset(match,0,sizeof(match));
	for(int i=1;i<=n;i++)
	{
		memset(used,0,sizeof(used));
		ret+=dfs(i);
	}
	return ret;
}

int main()
{
	int m,k,i,j,l,mth,u,v,miny,miny_id,flag,now;
	ll ans,cur;
	scanf("%d%d%d",&n,&m,&k);
	while(m--)
	{
		scanf("%d%d",&u,&v);
		mp[u][v]=1;
	}
	for(i=1;i<=k;i++)
	{
		scanf("%d%d",&vx[i],&vy[i]);
		prex[i]=prex[i-1]+vx[i];
	}
	mth=maxMatch();
	miny=1<<30;
	ans=-1,cur=0;
	for(i=1;i<=k;i++)
	{
		if(vy[i]<miny)
		{
			miny=vy[i];
			miny_id=i;
		}
		if(cur+prex[k]-prex[i]>ans)
		{
			ans=cur+prex[k]-prex[i];
			ansseq=curseq;
			for(j=1;j<=n;j++)
				if(!no_out[j])
					ansseq.push_back(make_pair(i,j));
			for(j=1;j<=n;j++)
				if(!no_in[j])
					ansseq.push_back(make_pair(i,-j));
		}
		cur+=vx[i];
		if(i+mth==n)
		{
			cur-=miny;
			flag=0;
			for(j=1;j<=n;j++)
			{
				if(no_out[j])
					continue;
				for(l=1;l<=n;l++)
				{
					tmp[l]=mp[j][l];
					mp[j][l]=0;
				}
				if(maxMatch()<mth)
				{
					curseq.push_back(make_pair(miny_id,j));
					flag=1;
					no_out[j]=1;
					break;
				}
				for(l=1;l<=n;l++)
					mp[j][l]=tmp[l];
			}
			for(j=1;j<=n&&!flag;j++)
			{
				if(no_in[j])
					continue;
				for(l=1;l<=n;l++)
				{
					tmp[l]=mp[l][j];
					mp[l][j]=0;
				}
				if(maxMatch()<mth)
				{
					curseq.push_back(make_pair(miny_id,-j));
					flag=1;
					no_in[j]=1;
					break;
				}
				for(l=1;l<=n;l++)
					mp[l][j]=tmp[l];
			}
			mth--;
		}
	}
	if(cur>ans)
	{
		ans=cur;
		ansseq=curseq;
	}
	printf("%d\n",(int)ansseq.size()+k);
	now=0;
	for(i=1;i<=k;i++)
	{
		while(now<ansseq.size()&&ansseq[now].first==i)
		{
			printf("%d ",ansseq[now].second);
			now++;
		}
		printf("0 ");
	}
	puts("");
}
  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值