视频讲解: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=k100−k 。易得当
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,100−k)100−k ,
b
=
k
g
c
d
(
k
,
100
−
k
)
b=\frac{k}{gcd(k,100-k)}
b=gcd(k,100−k)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 n−1 的区间,即选择区间 [ 1 , n − 1 ] [1,n-1] [1,n−1] 或区间 [ 2 , n ] [2,n] [2,n]。
接下来分几种情况讨论:
- 若原排列就是一个递增序列,则无需修改,共操作 0 0 0 次;
- 若 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,n−1] 排序即可,共操作 1 1 1 次;
- 若 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 次;
- 对于其他情况,则操作一次将 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
2m−xi 坐标的机器人向左走。
这样不断取出栈顶两个元素,计算他们的碰撞时刻,直到栈中的元素个数
≤
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|
∣i−j∣ 。
求最少的代价,使得原数组中值为
1
1
1 的元素,均变为值为
0
0
0 。
题解
理解题意后,我们发现就是为每一个
1
1
1 寻找一个独一无二的
0
0
0 。
我们将所有的
0
0
0 构成一个集合,所有的
1
1
1 构成另一个集合。同一集合内的元素两两无连边,不同集合间的元素有一条权值为
∣
i
−
j
∣
|i-j|
∣i−j∣ 的连边(
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|
∣x1−y1∣+∣x2−y2∣≤∣x1−y2∣+∣x2−y1∣
也就是说,对于左边的
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=dpi−1,j−1+∣xi−yj∣ ;
- 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=dpi−1,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(dpi−1,j,dpi−1,j−1+∣xi−yj∣)
实现时,可以利用滚动数组或调整 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(1≤n≤20) 个城市和
m
(
1
≤
m
≤
5
⋅
1
0
4
)
m(1 \leq m \leq 5 \cdot 10^4)
m(1≤m≤5⋅104) 个点,第
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(1≤di,j≤n+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(1≤i≤n) ,第
i
i
i 回合建造的城市与该节点的距离必须不小于
n
−
i
+
2
n-i+2
n−i+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=1∏nn−i+1sumn−i+2−i+1
那么该节点会被占领的概率为
1
−
p
1-p
1−p ,求所有节点被占领概率的总和即是被占领数量的数学期望。
参考代码
#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(2≤n≤50) 个点与 m ( 0 ≤ m ≤ n ( n − 1 ) 2 ) m(0 \leq m \leq \frac{n(n-1)}{2}) m(0≤m≤2n(n−1)) 条有向边构成的城堡。城堡中无法从一个点出发后,再回到这个点。
城堡会遭受
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,xi−yi∗ti),其中
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(1≤xi,yi≤109) 已知。
每次操作,可以删除某个节点的所有入边,或所有出边。删除操作永久有效,即之后的每波袭击中,这些边都会被删掉。
求成功防御 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 i≤n−mth 则防守成功,反之防守失败。
遇到防守失败时,需要进行防御操作。由于数据量较小,我们可以枚举每个点,尝试删除该节点的出边或入边,再跑一遍二分图来判断。
注意到,删除一个节点的所有出边或入边,等价与在二分图中删除一个节点时,其最大匹配数最多减少
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,xi−yi∗ti) ,即最小为
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("");
}