前言
这场比赛跟之前的ABC比赛比起来难度还是要高不少,前面的几题也没有之前的比赛那么签,中间的中档题也要难不少(个人感觉),下面的题解,我赛时过了的题我会尽量讲一下我做题时候的心理过程,是如何想出答案的,赛时没过但是后面补题了的题,我就尽量讲一下我对于题解的理解。
A.Online Shopping(模拟)
考察英语阅读理解
这题还是比较简单,就是网购,告诉我们要买的每个物品的数量和价格,以及满多少钱包邮,问最后付的钱多少,直接按题意模拟即可。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int p[maxn],q[maxn];
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,s,k;cin>>n>>s>>k;
int tot=0;
for(int i=1;i<=n;i++){
cin>>p[i]>>q[i];
tot+=p[i]*q[i];
}
if(tot<s) tot+=k;
cout<<tot<<endl;
return 0;
}
B.Glass and Mug(模拟)
题意
给我们玻璃杯和马克杯,然后有三种跟倒水相关的操作,总共执行 k k k次,问 k k k次后最终玻璃杯和马克杯还剩多少水。
分析
同样按照题意模拟这三种操作,可以用两个变量来记录当前的玻璃杯和马克杯的水量,然后三种操作就对应对这两个变量进行操作。不过要注意 i f if if语句的顺序一定要按照它给的三种操作顺序,(一开始没看懂第一种操作,我就先 i f if if第二种情况,后面再 e l s e i f else if elseif第一种情况,样例没过,才注意到一定要按照给的顺序判断)
代码
#include<bits/stdc++.h>
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,G,M;cin>>n>>G>>M;
int g=0,m=0;
for(int i=1;i<=n;i++){
if(g==G){
g=0;
}
else if(!m){
m=M;
}
else{
int tem=min(G-g,m);
m-=tem;
g+=tem;
}
}
cout<<g<<" "<<m<<endl;
return 0;
}
C.T-shirts(贪心)
题意
这题是真正的阅读理解
给一个含012的字符串,有两种衣服一般的和豪华的,一开始有
m
m
m件一般的衣服,1和2代表两种活动,1的活动两种衣服都可以穿,2的活动只能穿豪华衣服,0代表洗衣服,即两种衣服的数量恢复到原有的数量,问最少需要买多少件衣服才能满足活动需要
分析
首先因为豪华衣服在两种活动都能穿,所以贪心地想,要买衣服肯定买的是豪华衣服。然后字符串里每次遇到0就会重置衣服数量,所以肯定要以0为分界线,分成若干个段,满足最少买衣服数量且能满足这些段的要求。
然后赛时我一下子脑抽了,没想到在不确定买多少件衣服的情况下要怎么做,就写了个二分,二分购买豪华衣服的数量再
O
(
n
)
O(n)
O(n),然后就可以简单地遍历模拟是否符合条件。
正解是直接统计0划分出的若干个段中,1、2活动的数量分别为
x
x
x,
y
y
y,然后因为开始一件豪华衣服也没有,所以至少要买的数量大等于
y
y
y,而且如果活动1过多,自己原有的普通衣服不够穿,那么还需要买豪华衣服使得活动1也够穿,也就是购买的豪华衣服数量大等于
x
+
y
−
m
x+y-m
x+y−m,所以答案就是若干段中的
m
a
x
(
x
+
y
−
m
,
y
)
max(x+y-m,y)
max(x+y−m,y)的最大值。
代码(二分)
#include<bits/stdc++.h>
using namespace std;
int n,m;string str;
bool check(int mid){
int s=m,p=mid;
for(auto c:str){
if(c=='0'){
s=m;p=mid;
}
else if(c=='1'){
if(s) s--;
else if(p) p--;
else return 0;
}
else{
if(p) p--;
else return 0;
}
}
return 1;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
cin>>str;
int l=0,r=1e3,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid)) ans=mid,r=mid-1;
else l=mid+1;
}
cout<<ans<<endl;
return 0;
}
D.Swapping Puzzle(枚举/全排列)
题意
给两个 n ∗ m n*m n∗m的二维矩阵 ( n , m ≤ 5 ) (n,m\leq 5) (n,m≤5),可以执行若干次操作,每次操作将第一个矩阵的相邻两行或者相邻两列进行交换,问将矩阵一变成矩阵二至少需要经过多少次操作,或者不可能使矩阵一变成矩阵二。
分析
赛时这题没做出来,不过还是讲一下我赛时的心路历程。
我先去观察,交换行列对矩阵的影响是什么。会发现假如只交换列,那么对于行来说,下标相同的元素都是一块变换的,也就是同一行的元素只是在同一行内部移动,不会跑到其他行。同理交换行,对于每一列来说,元素只在每一列内部移动不会跑出去。
就算两种操作结合起来,依旧如此,所以判定是否有解的情况,我想的就是,能否将矩阵一的每一行对应矩阵二的某一行,并且满足这两行中的元素对应大小的数量相同,也就是矩阵二的这一行能够由矩阵一的这一行交换后得到。然后由这种交换得到一种次序映射关系,将矩阵二的这一行设为
1
,
2
,
3
,
4
,
5
1,2,3,4,5
1,2,3,4,5,得到矩阵一的这一行对应为
2
,
1
,
4
,
5
,
3
2,1,4,5,3
2,1,4,5,3(举例)等等,然后其他行也都能够对应映射到不同的某一行,且映射的数字也要为
2
,
1
,
4
,
5
,
3
2,1,4,5,3
2,1,4,5,3,代表这一种映射是有效的,有解。
但是会发现上面讲的这种匹配方法太麻烦,而且如果有重复数字的话也会不知道该将哪两个数字进行匹配。所以这题就卡在这里了,赛后看了题解恍然大悟。
正解不是通过矩阵一和矩阵二的元素去进行匹配,而是去枚举第二个矩阵所有行列交换后可能得到的矩阵,再检验矩阵一和矩阵二是否满足这种枚举的匹配。
对于行来说,所有行的排列方式为
n
!
n!
n!种,列同理为
m
!
m!
m!,我们相当于是去将矩阵二的行列所有可能的交换形式后得到的新矩阵去跟矩阵一匹配,而这所有可能的交换形式最多也就是
n
!
∗
m
!
n!*m!
n!∗m!即
125
∗
125
125*125
125∗125种。
PS:枚举全排列可以用c++的next_permutation()函数,不用写dfs,具体使用方法可以去搜一下,或者直接看我代码里面的用法。
具体的这种操作方法就可以开两个数组
r
o
w
[
n
]
row[n]
row[n]和
c
o
l
[
m
]
col[m]
col[m],分别表示矩阵二的第
i
i
i行,对应交换后得到的新矩阵第
r
o
w
[
i
]
row[i]
row[i]行,以及矩阵二的第
j
j
j行,对应交换后得到新矩阵的第
c
o
l
[
i
]
col[i]
col[i]列。
然后我们要做的就很简单了,就是检查这个新矩阵是否和矩阵一相等,直接
O
(
n
2
)
O(n^2)
O(n2)的遍历矩阵一,一个一个元素检查即可(发现有一个元素不满足直接
b
r
e
a
k
break
break,可以优化一点时间)
然后假如已经得到可以匹配了,代表一定有解,又该如何求总操作次数呢?我们不妨把行、列分开,因为行列之间的交换互不影响,那么单单看行,想要交换回去,其实就相当于把我们枚举到的排列
2
,
1
,
4
,
5
,
3
2,1,4,5,3
2,1,4,5,3通过相邻元素交换,使其变回
1
,
2
,
3
,
4
,
5
1,2,3,4,5
1,2,3,4,5,那么这不就是经典的冒泡排序吗?所以我们就可以知道操作次数就等于排列的逆序对数,再
O
(
n
2
)
O(n^2)
O(n2)的遍历统计枚举的排列的逆序对数即可。
将每一种可能的枚举排列的行、列的逆序对数相加,取
m
i
n
min
min,即为最终答案。总的时间复杂度是
O
(
n
!
∗
m
!
∗
n
∗
m
)
O(n!*m!*n*m)
O(n!∗m!∗n∗m),因为
n
,
m
≤
5
n,m\leq 5
n,m≤5所以复杂度是可以通过的。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e1;
int a[maxn][maxn],b[maxn][maxn];
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>b[i][j];
int row[6],col[6];
//初始化枚举的行列排列
for(int i=1;i<=n;i++)
row[i]=i;
for(int i=1;i<=m;i++)
col[i]=i;
int ans=1e9;
do{
do{
int flag=1;//表示这次枚举能否匹配
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
flag&=(a[row[i]][col[j]]==b[i][j]);
//有一个元素不等,&后flag就会为0
if(!flag) continue;//不匹配则跳过该次枚举
int tem=0;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
tem+=row[i]>row[j];//统计逆序对数
for(int i=1;i<=m;i++)
for(int j=i+1;j<=m;j++)
tem+=col[i]>col[j];
ans=min(ans,tem);
}while(next_permutation(row+1,row+1+n));//全排列枚举
}while(next_permutation(col+1,col+1+m));
if(ans==(int)1e9) ans=-1;//说明一个匹配都找不到
cout<<ans<<endl;
return 0;
}
E.Lucky bag(状压dp)
题意
有 N N N个物品,将其装入 D D D个袋子,袋子可以为空,求通过合理分配,所有袋子重量的最小方差。数据范围 2 ≤ D , N ≤ 15 2\leq D,N\leq 15 2≤D,N≤15
分析
看到数据范围应该要去猜这题是状压
d
p
dp
dp,然后考虑如何设计状态。
在这之前,先去看一下题目要求的是什么。方差
S
2
=
1
D
∑
i
=
1
D
(
x
i
−
x
‾
)
2
S^2=\frac{1}{D}\sum_{i=1}^{D}{(x_i-\overline{x})^2}
S2=D1i=1∑D(xi−x)2
对于给定的那些物品,每个袋子的平均重量
x
‾
\overline x
x是确定了的
∑
x
i
/
D
\sum x_i/D
∑xi/D,所以我们可以考虑设计状态
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示将状态
i
i
i表示的几个数字,装入
j
j
j个袋子,使得
∑
(
x
i
−
x
‾
)
2
\sum (x_i-\overline x)^2
∑(xi−x)2最小的值(不直接把除以
D
D
D也带上是因为如果中间就除以
D
D
D了,过程的状态转移就不好写了,所以放到最后再除)。那么最终要求的答案明显就是
d
p
[
11...1
]
[
D
]
/
D
dp[11...1][D]/D
dp[11...1][D]/D,即将所有的物品装入
D
D
D个袋子使得
∑
(
x
i
−
x
‾
)
2
\sum (x_i-\overline x)^2
∑(xi−x)2最小,再除以
D
D
D,就是最终的最小方差。
这里状态设计感觉不是很好想,不过按我个人经验,
d
p
dp
dp题经常可以从结果来逆推,如这题可以根据最终要的方差的形态来构造
d
p
dp
dp数组,再去尝试推
d
p
dp
dp数组的递推关系。
然后来考虑状态转移的过程,对于
d
p
[
S
]
[
i
]
dp[S][i]
dp[S][i],我们考虑它是由哪些状态转移过来的。
将
S
S
S里的物品放入
i
i
i个袋子,可以考虑先将
S
S
S里的部分物品
T
T
T(
T
∈
S
T\in S
T∈S)放入
i
−
1
i-1
i−1个袋子(
d
p
dp
dp中也经常是相邻两个
i
i
i之间进行转移)对应的贡献为
d
p
[
T
]
[
i
−
1
]
dp[T][i-1]
dp[T][i−1],再将剩下的物品
T
−
S
T-S
T−S放入一个袋子,对应贡献
d
p
[
T
−
S
]
[
1
]
dp[T-S][1]
dp[T−S][1],它们之间的贡献是可以直接相加的(这个地方我觉得是个重点,也是状态转移的核心)
所以就可以得到状态转移方程式为
d
p
[
S
]
[
i
]
=
m
i
n
{
d
p
[
T
]
[
i
−
1
]
+
d
p
[
T
−
S
]
[
1
]
}
,
∀
T
∈
S
dp[S][i]=min\{dp[T][i-1]+dp[T-S][1]\},\forall T\in S
dp[S][i]=min{dp[T][i−1]+dp[T−S][1]},∀T∈S
而所有的物品可能,放入一个袋子的
∑
(
x
i
−
x
‾
)
2
\sum (x_i-\overline x)^2
∑(xi−x)2可以轻松地算出来,也就是初始化
d
p
[
S
]
[
1
]
dp[S][1]
dp[S][1],然后就可以状压
d
p
dp
dp递推了。
这里还有一个知识点,就是对于
S
S
S,该如何去枚举子集
T
T
T,这里涉及到一个二进制子集枚举的知识点,有一个结论一样的,就是如果已知二进制
S
S
S,那么枚举子集
T
T
T就是不断地令
T
=
(
T
−
1
)
&
S
T=(T-1)\&S
T=(T−1)&S,
T
T
T就可以遍历所有的
S
S
S子集。
考虑时间复杂度,因为所有的状态有
2
n
2^n
2n,对于有
i
i
i个元素的集合,子集数量为
2
i
2^i
2i,相乘后再一顿
∑
\sum
∑,由二项式定理合并起来,反正最后是
O
(
3
n
∗
D
)
O(3^n*D)
O(3n∗D)
细节见代码
代码
#include<bits/stdc++.h>
#define db double
using namespace std;
const int N=16;
db arr[N],dp[1<<N][N];
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
memset(dp,0x7f,sizeof(dp));//初始赋极大值
int n,d;cin>>n>>d;
db ave=0;
for(int i=0;i<n;i++) cin>>arr[i],ave+=arr[i];
ave/=d;//计算平均数
for(int i=0;i<(1<<n);i++){
db sum=0;
for(int j=0;j<n;j++)
if(i&(1<<j)) sum+=arr[j];
dp[i][1]=pow(sum-ave,2);//初始化所有放入1个袋子的最小值
}
for(int i=2;i<=d;i++){
for(int j=0;j<(1<<n);j++){//枚举所有二进制状态
for(int k=j;k;k=(k-1)&j){//枚举状态j的所有子集
dp[j][i]=min(dp[j][i],dp[j-k][i-1]+dp[k][1]);
}
}
}
db ans=dp[(1<<n)-1][d]/d;
cout<<fixed<<setprecision(15)<<ans<<endl;
return 0;
}
F.Random Update Query(线段树)
题意
给一个数组,有多次操作,每次操作 L i , R i , X i L_i,R_i,X_i Li,Ri,Xi,表示将数组中 L i 到 R i L_i到R_i Li到Ri的区域等可能地选其中一个赋值为 X i X_i Xi,即对 ∀ i ∈ [ L i , R i ] \forall i\in [L_i,R_i] ∀i∈[Li,Ri],有 1 R i − L i + 1 \frac{1}{R_i-L_i+1} Ri−Li+11的概率使 a r r [ i ] = X i arr[i]=X_i arr[i]=Xi。求最终数组里每一个元素的期望值是多少。
分析
先去考虑每一次操作对于数组元素期望值的影响。
假如原先数组里
a
r
r
[
i
]
=
a
arr[i]=a
arr[i]=a,然后进行了一次操作
i
,
i
+
2
,
b
i,i+2,b
i,i+2,b,根据题意,数组里第
i
i
i个和第
i
+
1
i+1
i+1
i
+
2
i+2
i+2个元素各有
1
3
\frac{1}{3}
31的可能变为
b
b
b,对于
a
r
r
[
i
]
arr[i]
arr[i]而言,就是有
1
3
\frac13
31的可能变成
b
b
b,还有
1
−
1
3
=
2
3
1-\frac13=\frac23
1−31=32的可能保持不变仍是
a
a
a。
那么根据期望的定义,这一次操作后,
a
r
r
[
i
]
arr[i]
arr[i]的期望值就等于
∑
x
i
∗
p
i
\sum x_i*p_i
∑xi∗pi等于
1
3
∗
b
+
2
3
∗
a
\frac13*b+\frac23*a
31∗b+32∗a。
通过手算这一个例子,不难看出,对于一次操作
L
i
,
R
i
,
X
i
L_i,R_i,X_i
Li,Ri,Xi,对于数组期望的影响就是对于区间
[
L
i
,
R
i
]
[L_i,R_i]
[Li,Ri],先乘上
(
1
−
1
R
i
−
L
i
+
1
)
(1-\frac{1}{R_i-L_i+1})
(1−Ri−Li+11)(这里还需要用逆元化除为乘),即乘上
(
R
i
−
L
i
)
∗
(
R
i
−
L
i
+
1
)
m
o
d
−
2
(R_i-L_i)*(R_i-L_i+1)^{mod-2}
(Ri−Li)∗(Ri−Li+1)mod−2,也就是算出这一次操作后不变的期望,至于变化后的期望,由于都是
1
R
i
−
L
i
+
1
∗
X
i
\frac{1}{R_i-L_i+1}*X_i
Ri−Li+11∗Xi,所以相当于再给这个区间内所有数加上这个值。
所以我们需要一种能够快速给区间加上某个数,乘上某个数的数据结构,结合数据范围
N
,
M
≤
2
∗
1
0
5
N,M\leq 2*10^5
N,M≤2∗105,显然可以用线段树解决!
而且刚好洛谷里【模板】线段树2这道题就要求我们实现区间加和区间乘,所以可以直接用这道题的数据结构来实现。
代码
#include<bits/stdc++.h>
#define int long long
#define ll long long
#define pii pair<int,int>
#define inf 0x3f3f3f
const int maxn=2e5+10;
using namespace std;
ll arr[maxn];
int mod=998244353;
struct tree{
ll l,r;//区间左右端点
ll sum=0,laz1=0,laz2=1;//当前结点的值和懒标记
}t[4*maxn+2];
int ksm(int a,int b){
int res=1;
for(;b;b>>=1,a=a*a%mod)
if(b&1) res=res*a%mod;
return res;
}
void build(int p,int l,int r,int mod){//建树
t[p].l=l;t[p].r=r;
t[p].laz1=0;
t[p].laz2=1;
if(l==r){//是叶子结点
t[p].sum=arr[l];
return;
}
int mid=l+r>>1;
build(p*2,l,mid,mod);
build(p*2+1,mid+1,r,mod);
t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;//当前节点的值等于两个孩子的值的和
}
void spread(int p,int mod){
t[p*2].sum=(t[p*2].sum*t[p].laz2+t[p].laz1*(t[p*2].r-t[p*2].l+1))%mod;//更新孩子用懒标记修正后的值
t[p*2+1].sum=(t[p*2+1].sum*t[p].laz2+t[p].laz1*(t[p*2+1].r-t[p*2+1].l+1))%mod;
t[p*2].laz2=t[p*2].laz2*t[p].laz2%mod;//将该结点的懒标记传给孩子
t[p*2+1].laz2=t[p*2+1].laz2*t[p].laz2%mod;
t[p*2].laz1=(t[p*2].laz1*t[p].laz2+t[p].laz1)%mod;
t[p*2+1].laz1=(t[p*2+1].laz1*t[p].laz2+t[p].laz1)%mod;
t[p].laz1=0;//清空当前结点的懒标记
t[p].laz2=1;//清空当前结点的懒标记
}
void change(int p,int l,int r,int k,int mod){//区间增加k
if(l>t[p].r||r<t[p].l) return;
if(l<=t[p].l&&r>=t[p].r){//如果要修改的区间覆盖了当前结点表示的区间
t[p].sum+=(ll)k*(t[p].r-t[p].l+1)%mod;
t[p].laz1=(t[p].laz1+k)%mod;
return;
}
spread(p,mod);//检测懒标记
int mid=t[p].l+t[p].r>>1;
if(l<=mid) change(p*2,l,r,k,mod);//如果要修改的区间覆盖了左儿子,就修改左儿子
if(r>mid) change(p*2+1,l,r,k,mod);//右儿子同理
t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;//最终维护的值等于左儿子的值+右儿子的值
}
void change2(int p,int l,int r,int k,int mod){//区间乘法
if(l>t[p].r||r<t[p].l) return;
if(l<=t[p].l&&r>=t[p].r){//如果要修改的区间覆盖了当前结点表示的区间
t[p].sum=(t[p].sum*k)%mod;
t[p].laz2=(t[p].laz2*k)%mod;
t[p].laz1=(t[p].laz1*k)%mod;
return;
}
spread(p,mod);//检测懒标记
int mid=t[p].l+t[p].r>>1;
if(l<=mid) change2(p*2,l,r,k,mod);//如果要修改的区间覆盖了左儿子,就修改左儿子
if(r>mid) change2(p*2+1,l,r,k,mod);//右儿子同理
t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;//最终维护的值等于左儿子的值+右儿子的值
}
ll ask(int p,int l,int r,int mod){
if(l<=t[p].l && r>=t[p].r) return t[p].sum%mod;//如果被覆盖,就返回维护的值
spread(p,mod);//下传懒标记,并查询左右儿子
int mid=t[p].l+t[p].r>>1;
ll ans=0;
if(l<=mid) ans=(ans+ask(p*2,l,r,mod))%mod;
if(r>mid) ans=(ans+ask(p*2+1,l,r,mod))%mod;//累加答案,返回左右儿子的和
return ans%mod;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++) cin>>arr[i];
build(1,1,n,mod);
for(int i=1;i<=m;i++){
int l,r,x;cin>>l>>r>>x;
int mu=ksm(r-l+1,mod-2);
int tem=x*mu%mod;
int tem2=(1-mu+mod)%mod;
change2(1,l,r,tem2,mod);
change(1,l,r,tem,mod);
}
for(int i=1;i<=n;i++)
cout<<ask(1,i,i,mod)<<" ";
//system("pause");
return 0;
}