ABC416 解题报告
标 * 的为赛后补题。
A - Vacation Validation
解题思路
签到题,直接检查 \([L,R]\) 中间是否有不是 o
的字符,如果有,输出 No
并退出,否则输出 Yes
。
参考代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,l,r;
string s;
int main()
{
cin>>n>>l>>r>>s;
s=' '+s;
for(int i=l;i<=r;i++)
{
if(s[i]!='o')
{
cout<<"No\n";
return 0;
}
}
cout<<"Yes\n";
return 0;
}
B - 1D Akari
题目大意
给定由 .
和 #
构成的字符串,将尽量多的 .
改为 o
使得任意两个 o
之间至少有一个 #
。
解题思路
ABC 最难的 B 题。 根据题目条件,只需要让每个 o
和下一个 o
之间至少有一个 #
即可。构造每一个 .
连续段都选一个 .
改为 o
即可获得答案。
参考代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 105
using namespace std;
int n;
char c[N],s[N];
int main()
{
scanf("%s",c+1);
s[0]='#';
n=strlen(c+1);
for(int i=1;i<=n;i++)
{
if(c[i]=='#') s[i]='#';
else if(s[i-1]=='#') s[i]='o';
else s[i]='.';
}
printf("%s\n",s+1);
return 0;
}
C - Concat (X-th)
题目大意
给定 \(N\) 个字符串 \(S_1 \sim S_n\),求所有的 \(S_{t_1}+S_{t_2}+\cdots+S_{t_K}\) 中字典序第 \(X\) 小的字符串。\(t_i\) 为 \([1,N]\) 中的任意值。
解题思路
考虑到总共有 \(N^K\) 种组合,而 \(N^K\leq 10^5\) ,直接暴力枚举然后排序即可。
参考代码
点击查看代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<vector>
#define N 15
using namespace std;
int n,k,x;
string s[N];
vector<string> v;
void dfs(int dep,string c)
{
if(dep==k+1)
{
v.push_back(c);
return;
}
for(int i=1;i<=n;i++)
{
dfs(dep+1,c+s[i]);
}
return;
}
int main()
{
cin>>n>>k>>x;
for(int i=1;i<=n;i++) cin>>s[i];
dfs(1,"");
sort(v.begin(),v.end());
cout<<v[x-1]<<'\n';
return 0;
}
D - Match, Mod, Minimize 2
题目大意
给定两个长度为 \(N\) 的数组 \(A,B\),可以将两个数组任意排列,求任意排列后 \(\sum_{i=1}^{N}((A_i+B_i) \mod M)\) 的最小值。
解题思路
放宽条件,考虑把 \(\mod M\) 去掉,那么答案是定值。如果把 \(\mod M\) 加上,那么如果 \(A_i+B_i\ge M\) ,会让这个定值减少 \(M\)。题目转换为最大化 \(A_i+B_i\ge M\) 的对数
转换成这样就好做了。贪心,对 \(A\),\(B\) 进行排序,从大到小枚举每一个 \(B_i\),在 \(A\) 中找最小的 \(A_j\) 使得 \(A_i+B_j\geq M\) 且 \(A_j\) 未被匹配,如果能找到,答案减 \(M\),否则此时的答案就是最小值。\(i,j\) 可以双指针维护。
时间复杂度为 \(O(N \log N)\),瓶颈在于排序。
参考代码
点击查看代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define N 300005
using namespace std;
long long n,m,a[N],b[N],ans;
void solve()
{
ans=0;
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]),ans+=a[i];
for(int i=1;i<=n;i++) scanf("%lld",&b[i]),ans+=b[i];
sort(a+1,a+n+1);
sort(b+1,b+n+1);
for(int l=1,r=n;r>=1;r--)
{
while(l<=n&&a[l]+b[r]<m) l++;
if(l==n+1) break;
ans-=m;
l++;
}
printf("%lld\n",ans);
return;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
solve();
}
return 0;
}
/*
6
1 3 4
0 1 2
*/
E - Development
题目大意
给定一个 \(N\) 个点 \(M\) 条边的简单无向图,边有边权,经过一条边需耗费对应边权的时间。此外有 \(K\) 个点被改建为机场,可以耗费 \(T\) 的时间从一个机场到任意一个机场。\(Q\) 次操作,每次操作会添加一条边,改建一个机场或询问 \(sum=\sum_{i=1}^N\sum_{j=1}^{N}f_{i,j}\),其中 \(f_{i,j}\) 表示 \(i\) 到 \(j\) 的最短路,不联通记为 \(0\)。
解题思路
注意,下文中,对于不联通的 \((i,j)\),\(f_{i,j}\) 记为 \(\inf\)。
发现数据范围允许 \(O(N^2Q)\) 和 \(O(N^3)\),这使得本题的可操作空间变大。初始时,如果将机场视作 \(K\) 阶完全图,可以跑 floyd 求出所有点对最短路,得到初始的 \(f\)。
对于加边操作,考虑发现边 \((x,y)\) 对 \(f_{i,j}\) 的影响。一共两种情况:经过这条边,或不经过这条边。对应的式子为 \(f_{i,j}=\min(f_{i,j},\min(f_{i,x}+w_{x,y}+f_{y,j},f_{i,y}+w_{y,x}+f_{x,j}))\),对每一对 \((i,j)\) 都更新 \(f_{i,j}\),复杂度为 \(O(N^2)\)。
对于加机场操作,考虑机场对 \(f_{i,j}\) 的贡献,记 \(g_i\) 为 \(i\) 与最近的机场的距离,如果路上乘坐飞机,那么一定是在与 \(i\) 最近的机场上飞机,与 \(j\) 最近的机场下飞机,那么加机场后,先 \(O(NK)\) 地更新 \(g\),再更新 \(f_{i,j}=\min(f_{i,j},g_i+T+g_j)\)。
对于询问操作,可以 \(O(N^2)\) 暴力枚举,也可以在更新 \(f\) 时动态维护,参考代码使用后者。
总时间复杂度为 \(O(N^2Q)\)。
参考代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define inf 0x3f3f3f3f3f3f3f3f
#define N 505
using namespace std;
long long n,m,k,t,ans,q;
long long f[N][N],d[N],minn[N];
bool vis[N];
void change(int x,int y,long long dis)
{
if(f[x][y]<=dis) return;
if(f[x][y]>=inf) ans+=dis;
else ans-=(f[x][y]-dis);
f[x][y]=dis;
return;
}
void add_edge(int x,int y,int c)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) if(i!=j)
{
change(i,j,min(f[i][x]+f[y][j]+c,f[i][y]+f[x][j]+c));
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=k;j++)
{
minn[i]=min(minn[i],f[i][d[j]]);
}
}
return;
}
void add_airport(int x)
{
if(vis[x]) return;
d[++k]=x;
vis[x]=true;
for(int i=1;i<=n;i++) minn[i]=min(minn[i],f[i][x]);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++) if(i!=j)
{
change(i,j,minn[i]+minn[j]+t);
}
}
return;
}
int main()
{
memset(f,0x3f,sizeof(f));
memset(minn,0x3f,sizeof(minn));
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) f[i][i]=0;
for(int i=1;i<=m;i++)
{
long long x,y,c;
scanf("%lld%lld%lld",&x,&y,&c);
f[x][y]=min(f[x][y],c);
f[y][x]=f[x][y];
}
scanf("%lld%lld",&k,&t);
for(int i=1;i<=k;i++) scanf("%lld",&d[i]),vis[d[i]]=true;
for(int i=1;i<=k;i++)
{
for(int j=1;j<=k;j++) if(i!=j)
{
f[d[i]][d[j]]=min(f[d[i]][d[j]],t);
}
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
ans+=(f[i][j]<inf?f[i][j]:0);
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=k;j++)
{
minn[i]=min(minn[i],f[i][d[j]]);
}
}
scanf("%lld",&q);
while(q--)
{
int op;
scanf("%d",&op);
if(op==1)
{
int x,y,t;
scanf("%d%d%d",&x,&y,&t);
add_edge(x,y,t);
}
else if(op==2)
{
int x;
scanf("%d",&x);
add_airport(x);
}
else
{
printf("%lld\n",ans);
}
}
return 0;
}
*F - Paint Tree 2
题目大意
求大小为 \(N\) 的树上,\(K\) 条不相交的链所覆盖的点权之和最大值。
解题思路
参考 luogu id:Moya_Rao 大佬的题解。
考虑树形 DP。设计状态 \(f_{i,j,k}\) 表示子树 \(i\) 内有 \(j\) 条链,点 \(i\) 的状态为 \(k\) 的点权之和的最大值。\(k\) 的定义如下:
- \(k=0\):子树 \(i\) 中没有一条链经过 \(i\)。
- \(k=1\):子树 \(i\) 中有一条链以 \(i\) 为端点。
- \(k=2\):子树 \(i\) 中有一条链经过 \(i\) 但不以 \(i\) 为端点。
考虑转移。将子树 \(v\) 转移给子树 \(u\) 的情况有三种:
-
毫不相干:没有一条链经过 \((u,v)\),此时 \(f_{u,i+j,p} \leftarrow f_{u,i,p}+f_{u,j,q}\ (i+j\leq K,0\leq p,q <3)\)。
-
以 \(u\) 为端点:在 \(v\) 中的一条以 \(v\) 为端点的链延伸到 \(u\),此时 \(f_{u,i+j,1}\leftarrow f_{u,i,0}+f_{v,j,1}+w_u\ (i+j\leq K)\)。
-
链经过 \(u\):两条分别以 \(u\) 为端点和以 \(v\) 为端点的点连接起来,组成新的链,此时 \(f_{u,i+j-1,2} \leftarrow f_{u,i,1}+f_{v,j,1}\ (i+j-1\leq K,i+j\geq 1)\)。
初始值为 \(f_{u,0,0}=0,f_{u,1,1}=w_u,f_{u,1,2}=w_u\),注意转移时用临时数组存储一下(树形 DP 最好都这样),复杂度 \(O(9NK)\)。
参考代码
点击查看代码
#include<iostream>
#include<cstring>
#include<cstdio>
#define N 200005
using namespace std;
long long n,K,head[N],to[N*2],nxt[N*2],w[N];
void add(int x,int y)
{
static int now=0;
now++;
to[now]=y;
nxt[now]=head[x];
head[x]=now;
return;
}
long long f[N][10][3],ff[10][3];
void dfs(int x,int fa)
{
f[x][0][0]=0;
f[x][1][1]=w[x];
f[x][1][2]=w[x];
for(int i=head[x];i;i=nxt[i])
{
int v=to[i];
if(v==fa) continue;
dfs(v,x);
memset(ff,-0x3f,sizeof(ff));
for(int j=0;j<=K;j++) for(int k=0;k<=K;k++)
{
if(k+j<=K) for(int p=0;p<3;p++) for(int q=0;q<3;q++)
{
ff[j+k][p]=max(ff[j+k][p],f[x][j][p]+f[v][k][q]);
}
if(k+j<=K) ff[j+k][1]=max(ff[j+k][1],f[x][j][0]+f[v][k][1]+w[x]);
if(k+j-1<=K&&k+j>0) ff[j+k-1][2]=max(ff[j+k-1][2],f[x][j][1]+f[v][k][1]);
}
for(int j=0;j<=K;j++)
{
for(int p=0;p<3;p++)
{
f[x][j][p]=max(f[x][j][p],ff[j][p]);
if(x==11&&j==2&&p==0) cerr<<'*';
}
}
}
return;
}
int main()
{
memset(f,-0x3f,sizeof(f));
scanf("%lld%lld",&n,&K);
for(int i=1;i<=n;i++) scanf("%lld",&w[i]);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
dfs(1,0);
long long ans=-0x3f3f3f3f3f3f3f3f;
for(int i=0;i<=K;i++)
{
for(int j=0;j<3;j++)
{
ans=max(ans,f[1][i][j]);
}
}
printf("%lld\n",ans);
return 0;
}
*G - Concat (1st)
题目大意
同 C 题,但 \(N,K\) 的数据范围扩大,\(X\) 恒为 \(1\)。
解题思路
参考 luogu id:Moya_Rao 大佬的题解(没错,又是这位巨佬)和官方题解。
考虑看到该题的第一反应是什么,大部分人会想到对 \(S\) 排序完直接硬钢,但这样连样例三都过不了。观察样例三,发现之所以排序不是最优的,是因为 \(\texttt{c+c}\) 并不比 \(\texttt{cba+c}\) 来得优秀,\(\texttt{c}\) 不应排在 \(\texttt{cba}\) 前面。
考虑换一种比较方式 \(\prec\),\(a\prec b\) 当且仅当 \(a+b<b+a\) 或 \(a+b=b+a \operatorname{and} a<b\),根据官方题解,这样的偏序关系满足传递性,因此,我们就可以依此进行排序。
现在计算答案。贪心地想,我们希望最小的字符串 \(S_{min}\) 重复多次,事实上,\(K=\inf\) 时答案就是 \(S_{min}\) 的循环。然而 \(K\neq\inf\)。因此,我们假设当前的最优解为 \(S_{min}\) 的循环,维护最小的后缀 \(ans\),初始为空串,每次令 \(ans=S_{min}+ans\),并不断用其余的 \(S\) 替代 \(S_{min}\) 看看能不能得到更短的后缀更新 \(ans\)。\(|ans|\ge|S_{min}|\) 时停止,此时 \(S_{min}\) 的循环加上 \(ans\) 即为答案。
参考代码
点击查看代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define N 100005
using namespace std;
int n,k;
string s[N];
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>s[i];
sort(s+1,s+n+1,[](string a,string b){
return (a+b)!=(b+a)?(a+b)<(b+a):a<b;
});
string ans="";
while(k&&ans.size()<s[1].size())
{
k--;
string tmp=s[1]+ans;
for(int i=2;i<=n;i++) tmp=min(tmp,s[i]+ans);
ans=tmp;
}
while(k--) cout<<s[1];
cout<<ans;
return 0;
}