比赛传送门
作者: fn
签到题
C题 Concatenation / 连接
题目大意
给定
n
n
n 个字符串,将他们拼接起来。
求结果的字典序最小的方案。
1
≤
N
≤
2
∗
1
0
6
1≤N≤2∗10^6
1≤N≤2∗106
考察内容
排序,复杂度优化
分析
解法不唯一。
一个 O ( n l o g n ) O(nlogn) O(nlogn) 的做法:直接sort排序。
可选的常数优化:
- 定义时限定string的大小。
- 使用快读。
#include<bits/stdc++.h>
using namespace std;
string s[int(2e6+5)];
bool cmp(string &a,string &b){
return a+b<b+a;
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>s[i];
sort(s,s+n,cmp);
for(int i=0;i<n;i++)
cout<<s[i];
return 0;
}
基本题
A题 Ancestor / 祖先
题目大意
给出两棵结点编号
1
−
n
1-n
1−n 的树A、B,A、B树上每个节点均有一个权值,给出
k
k
k 个关键点的编号
𝑥
1
…
𝑥
𝑛
𝑥_1…𝑥_𝑛
x1…xn 。
问有多少种方案,使得去掉恰好一个关键点使得剩余关键点在树A上LCA的权值大于树B上LCA的权值。
考察内容
lca,dfs序,模拟
分析
模板题。
已知多个结点的lca就是dfs序第一个结点和最后一个结点的lca。
预处理好dfs序,前两个结点,后两个结点,然后枚举删去的结点,累加答案即可。
#include<bits/stdc++.h>
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=1e5+5;
ll n,m,k,a[N],pa[N],b[N],pb[N];
ll x[N];
vector<int> ga[N],gb[N];
int dfs_[200020],len;
void dfsa(int u,int fa){
dfs_[++len]=u;
int sz=ga[u].size();
for(int i=0;i<sz;i++){
if(ga[u][i]!=fa){
dfsa(ga[u][i],u);
}
}
}
void dfsb(int u,int fa){
dfs_[++len]=u;
int sz=gb[u].size();
for(int i=0;i<sz;i++){
if(gb[u][i]!=fa){
dfsb(gb[u][i],u);
}
}
}
ll xa1=-1,xa2=-1,xa3=-1,lastxa=-1;
ll xb1=-1,xb2=-1,xb3=-1,lastxb=-1;
map<int,bool> mp;
int h[N],e[N*2],ne[N*2],idx;
int f[N][21];
int d[N];
void add(int a,int b) // 存边
{
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
void bfs(int u) // 预处理
{
queue<int> q; q.push(u),d[u] = 1;
while(!q.empty()){
int x = q.front(); q.pop();
for(int i=h[x]; ~i; i = ne[i]){
int y = e[i];
if(d[y]) continue; // 已经更新过的点 不需要在更新了
d[y] = d[x]+1;
f[y][0] = x;
for(int j = 1; j<=20; j++) // 这里懒了 没有写log判断树的最高高度
f[y][j] = f[f[y][j-1]][j-1];
q.push(y); // 把当前点放入 队列中
}
}
}
int lca(int a,int b){ // 求a,b的lca
if(d[a]<d[b]) swap(a,b);
for(int i=20; i>=0; i--)
if(d[f[a][i]]>=d[b]) // 找到和b同高度的节点
a = f[a][i];
if(a==b) return a;
for(int i=20; i>=0; i--)
if(f[a][i]!=f[b][i]) a= f[a][i],b = f[b][i]; // 往上找有共同的根的位置
return f[a][0];
}
ll ansa[N];
ll ansb[N];
int main(){
memset(h, -1, sizeof h); // 初始化头节点
cin>>n>>k; // 每棵树n个结点,k个关键结点
for(int i=1;i<=k;i++){
cin>>x[i]; // 给定k个关键结点
mp[x[i]]=1;
}
// 输入两棵树
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n-1;i++){
cin>>pa[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
for(int i=1;i<=n-1;i++){
cin>>pb[i];
}
len=0;
for(int i=1;i<=n-1;i++){
int from,to;
from=i+1; to=pa[i];
ga[from].push_back(to);
ga[to].push_back(from);
add(from,to); add(to,from); // 双向边
}
bfs(1); // 对a树进行lca的预处理,根为1
dfsa(1,0);
for(int i=1;i<=len;i++){
if(mp[dfs_[i]]){
if(xa1==-1){
xa1=dfs_[i]; // 记录a的dfs序中第一个x
}
else if(xa2==-1){
xa2=dfs_[i]; // 记录a的dfs序中第2个x
break;
}
}
}
for(int i=len;i>=1;i--){
if(mp[dfs_[i]]){
if(lastxa==-1){
lastxa=dfs_[i]; // 记录a的dfs序中最后一个x
}
else if(xa3==-1){
xa3=dfs_[i]; // 记录a的dfs序中倒数第2个x
break;
}
}
}
ll t1=a[lca(xa1,lastxa)]; // 两边的lca结点的权重
for(int i=1;i<=k;i++){ // 枚举删除每个结点
if(x[i]==xa1){
ansa[i]=a[lca(xa2,lastxa)];
}
else if(x[i]==lastxa){
ansa[i]=a[lca(xa1,xa3)];
}
else{ // 删一个中间结点,lca值是两边的lca结点的权重
ansa[i]=t1;
}
}
// 初始化
memset(h, -1, sizeof h); // 初始化头节点
memset(e,0,sizeof e);
memset(ne,0,sizeof ne);
idx=0;
memset(f,0,sizeof f);
memset(d,0,sizeof d);
len=0;
for(int i=1;i<=n-1;i++){
int from,to;
from=i+1; to=pb[i];
gb[from].push_back(to);
gb[to].push_back(from); // 双向边
add(from,to); add(to,from); // 双向边
}
bfs(1); // 对b树进行lca的预处理,根为1
dfsb(1,0);
for(int i=1;i<=len;i++){
if(mp[dfs_[i]]){
if(xb1==-1){
xb1=dfs_[i]; // 记录b的dfs序中第一个x
}
else if(xb2==-1){
xb2=dfs_[i]; // 记录b的dfs序中第2个x
break;
}
}
}
for(int i=len;i>=1;i--){
if(mp[dfs_[i]]){
if(lastxb==-1){
lastxb=dfs_[i]; // 记录b的dfs序中最后一个x
}
else if(xb3==-1){
xb3=dfs_[i]; // 记录b的dfs序中倒数第2个x
break;
}
}
}
t1=b[lca(xb1,lastxb)]; // 两边的lca结点的权重
for(int i=1;i<=k;i++){ // 枚举删除每个结点
if(x[i]==xb1){
ansb[i]=b[lca(xb2,lastxb)];
}
else if(x[i]==lastxb){
ansb[i]=b[lca(xb1,xb3)];
}
else{ // 删一个中间结点,lca值是两边的lca结点的权重
ansb[i]=t1;
}
}
// 树上多个点的LCA,就是DFS序最小的和DFS序最大的这两个点的LCA。
ll ans=0; // 答案为删除一个数的解的数量
for(int i=1;i<=k;i++){ // 枚举删除每个结点
if(ansa[i]>ansb[i]){
ans++;
}
}
cout<<ans<<endl;
}
/*
5 3
5 4 3
6 6 3 4 6
1 2 2 4
7 4 5 7 7
1 1 3 2
*/
进阶题
J题 Journey / 旅行
题目大意
给定若干个十字路口,右转不需要等红灯,直行、左转和掉头都需要等一次红灯。
求起点到终点最少等几次红灯。
考察内容
最短路径,dijkstra算法,双端队列bfs
分析
方法一(更直观):
把每条路看做点,十字路口处连边,形成一个边权为0/1的有向图。然后dijkstra算法求最短路。
方法一代码:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
typedef tuple<int, int, int> tup;
const int N = 1000005;
int n, m, cross[N][4], dis[N][4];
bool vis[N][4];
unordered_map<int, int> id[N];
int st1, st2, en1, en2;
int main() {
ios::sync_with_stdio(false), cin.tie(0);
// 读入数据
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 4; j++) {
cin >> cross[i][j];
if (cross[i][j]) id[i][cross[i][j]] = j;
}
}
cin >> st1 >> st2 >> en1 >> en2;
memset(dis, 0x3f, sizeof(dis));
int idx = id[st2][st1];
dis[st2][idx] = 0;
priority_queue<tup, vector<tup>, greater<tup>> PQ;
tup t0(0, st2, idx);
PQ.push(t0);
// PQ.push({0, st2, idx});
while (!PQ.empty()) {
tup t1=PQ.top();
int d=get<0>(t1), u=get<1>(t1), x=get<2>(t1);
// auto [d, u, x] = PQ.top();
PQ.pop();
if (vis[u][x]) continue;
vis[u][x] = true;
for (int i = 0; i < 4; i++) {
int delta = (i != (x + 1) % 4);
int v = cross[u][i];
if (!v) continue;
int j = id[v][u];
if (d + delta < dis[v][j]) {
dis[v][j] = d + delta;
tup t2(dis[v][j], v, j);
PQ.push(t2);
// PQ.push({dis[v][j], v, j});
}
}
}
int ans = dis[en2][id[en2][en1]];
if (ans > n * 4) {
ans = -1;
}
cout << ans;
}
方法二(更快):
不建图,直接在所给数据上用双端队列BFS解决,用一个deque维护队列。
方法二代码:
#include<bits/stdc++.h>
using namespace std;
using pii=pair<int,int>;
const int maxn=5e5+7;
struct node{int x,y,step;};
int n,sx,sy,tx,ty,ans,p[5];
int v[maxn][4],mp[maxn][4];
int main(){
// 读入数据
cin>>n;
for(int i=1;i<=n;i++)
for(int j=0;j<4;j++){
cin>>v[i][j];
mp[i][j]=maxn;
}
cin>>sx>>sy>>tx>>ty;
deque<node> q; // 双端队列
q.push_front({sx,sy,0});
while(q.size()){
node n1=q.front();
int x=n1.x, y=n1.y, step=n1.step;
q.pop_front();
if(y==0)
continue;
int j;
for(int i=0;i<4;i++){
if(v[x][i]!=y)
continue;
j=i;
}
if(mp[x][j]<=step)
continue;
else
mp[x][j]=step;
for(int i=0;i<4;i++){
if(v[y][i]!=x)
continue;
j=(i+1)%4; // 右转
}
for(int i=0;i<4;i++){
if(i==j)
q.push_front({y,v[y][i],step}); // 右转的放队列前面
else
q.push_back({y,v[y][i],step+1}); // 其他的方向步数+1,并且放队列后面
}
}
for(int i=0;i<4;i++)
if(v[tx][i]==ty)
ans=mp[tx][i];
if(ans==maxn)
ans=-1;
cout<<ans<<"\n";
return 0;
}
H题 Hacker / 黑客
题目大意
给出长度为
n
n
n 的小写字符串
A
A
A 和
k
k
k 个长度为
m
m
m 的小写字符串
𝐵
1
…
𝐵
𝑘
𝐵_1…𝐵_𝑘
B1…Bk ,
𝐵
𝐵
B 的每个位置拥有统一的权值
𝑣
1
…
𝑣
𝑚
𝑣_1…𝑣_𝑚
v1…vm 。
对每个 𝐵 𝑖 𝐵_𝑖 Bi 求最大区间和,要求该区间构成的字符串是 A A A 的子串。空区间合法。
考察内容
字符串匹配,后缀自动机,最大区间和
分析
我们可以将问题进行转化,相当于对
𝐵
𝑖
𝐵_𝑖
Bi 的每个位置求出它作为结束位置在
A
A
A 中的最长子串长度,然后在该区间求最大子段和,所有位置的最大值即为答案。
对于每个位置的最长子串,可以对
A
A
A 建后缀自动机,然后
𝐵
𝑖
𝐵_𝑖
Bi 从左往右在
A
A
A 的后缀自动机上转移,如果当前节点无法转移跳至父亲节点,最后无法转移则长度为0,转移成功则为转移前节点的最大长度+1。
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int maxn=1e6+5,maxm=1e6+5;
struct ad{
int ch[26],len,fa;
}node[maxn];
int n,m,k,lst=1,tot=1,b[maxn];
char s[maxn],a[maxm];
inline int read(){
int d=0,f=0,c=getchar();
for(;c<48||c>57;c=getchar()) f|=c=='-';
for(;c>47&&c<58;c=getchar()) d=(d<<1)+(d<<3)+(c^48);
return f?-d:d;
}
void add(int x){
int p=lst,np=lst=++tot,q,nq;
node[np].len=node[p].len+1;
for (;p&&!node[p].ch[x];p=node[p].fa) node[p].ch[x]=np;
if (!p) node[np].fa=1;
else{
q=node[p].ch[x];
if (node[q].len==node[p].len+1) node[np].fa=q;
else{
nq=++tot;node[nq]=node[q];node[nq].len=node[p].len+1;
for (;p&&node[p].ch[x]==q;p=node[p].fa) node[p].ch[x]=nq;
node[q].fa=node[np].fa=nq;
}
}
}
int main(){
// 读入数据
n=read();m=read();k=read();
scanf("%s",s+1);
for (int i=1;i<=n;++i) add(s[i]-'a');
for (int i=1;i<=m;++i) b[i]=read();
while (k--){
scanf("%s",a+1);
ll ans=0,sum=0,p=1;
for (int i=1;i<=m;++i){
if (sum+b[i]>=0&&node[p].ch[a[i]-'a']){
p=node[p].ch[a[i]-'a'];
sum+=b[i];
}
else{
p=1;
sum=0;
}
ans=max(ans,sum);
}
printf("%lld\n",ans);
}
return 0;
}