2025 寒假集训 第一期
J - Adventurers
题意:在一个二维平面上有n个点,选择一个新的原点使四个象限点数量的最小值最大化,求该值和新原点坐标
思路:看到最小值最大化,首先想到二分答案 \(a\),然后考虑该如何Check。
我一开始的想法是枚举 \(x\) 轴坐标 \(x_0\) 然后根据一个象限的点数为 \(a\) 确定原点坐标 \(y_0\) ,之后再分别check剩下三个象限是否满足条件,这样二维数点不是很好写,复杂度也应该会在 \(log^3\) 以上,所以行不通。
再进一步去考虑,二维的扫点可以用扫描线的想法去做,我们用两颗线段树维护 \(x_0\) 左右的点,枚举 \(x_0\) 时左树加点,右树删点,检查时每次可以分别二分出左右下半部分满足条件的最小 \(y\) 坐标分别记为 \(y_1,y_2\) 。那么我们最终的坐标必须为 \(y_0=max(y_1,y_2)\) ,我们只需在左右线段树上check \(y_0\) 上半部分即可,二分 \(y\) 坐标也可以在线段树上二分,这样复杂度就降低为 \(O(nlog^2n)\)
解法:首先将 \(x,y\) 坐标离散化,然后往右侧线段树上加点
为了扫描线的进行,我们将枚举 \(x_0\) 放外层,在内层进行二分答案 \(ans\) 的操作
Check的时候我们先在线段树上二分,求出 \(y_1,y_2\) ,在用 \(y_0=max(y_1,y_2)\) 去检查是否满足条件,满足的话将 \(y_0\) 和 \(ans\) 同时记录
最终求得 \(ans\) 最大值和相应的 \(x_0 , y_0\) 。
代码:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define unmap unordered_map
#define unset unordered_set
#define MAXQ priority_queue
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define frep(i,a,b) for(int i=a;i<(b);++i)
using namespace std;
template<typename T> using MINQ=priority_queue<T,vector<T>,greater<T> >;
using pii=pair<int,int>;
using vi=vector<int>;
using vii=vector<vi>;#define maxn 1000010
#define L(node) (ls[node])
#define R(node) (rs[node])int n,L,H,tot;
int lt,rt;
int tree[maxn],ls[maxn],rs[maxn];
int x[maxn],y[maxn],lx[maxn],ly[maxn];
vector<int>line[maxn];void update(int &node,int begin,int end,int x,int val){if(!node)node=++tot;if(begin==end){tree[node]+=val;return;}int mid=(begin+end)>>1;if(x<=mid)update(L(node),begin,mid,x,val);if(x>mid) update(R(node),mid+1,end,x,val);tree[node]=tree[L(node)]+tree[R(node)];
}
int query(int node,int begin,int end,int x){if(!node)return 0;if(begin==end){return tree[node];}int mid=(begin+end)>>1;if(x<=mid)return query(L(node),begin,mid,x)+tree[R(node)];else return query(R(node),mid+1,end,x);
}
int find(int node,int begin,int end,int x){ //线段树二分if(begin==end){return x<=tree[node]?begin:-1;}int mid=(begin+end)>>1;if(x<=tree[L(node)]) return find(L(node),begin,mid,x);else return find(R(node),mid+1,end,x-tree[L(node)]);
}
int check(int x){int u=find(lt,1,H,x),d=find(rt,1,H,x); //查找左下和右下最小坐标if(u<0||d<0||max(u,d)==H)return -1;if(query(lt,1,H,max(u,d)+1)<x)return -1;if(query(rt,1,H,max(u,d)+1)<x)return -1;return max(u,d)+1; //返回符合的y坐标
}
void solve(){cin>>n;L=0,H=0;tot=0;lt=rt=0;for(int i=1;i<=n;i++)cin>>x[i]>>y[i],lx[++L]=x[i],ly[++H]=y[i];//离散化sort(lx+1,lx+L+1),L=unique(lx+1,lx+L+1)-lx-1;sort(ly+1,ly+H+1),H=unique(ly+1,ly+H+1)-ly-1;for(int i=1;i<=n;i++)x[i]=lower_bound(lx+1,lx+L+1,x[i])-lx;for(int i=1;i<=n;i++)y[i]=lower_bound(ly+1,ly+H+1,y[i])-ly;//扫描线for(int i=1;i<=n;i++){line[x[i]].push_back(y[i]);update(rt,1,H,y[i],1);}int ANS=0,X=0,Y=0;for(int i=1;i<=L;i++){ //枚举x0int l=0,r=n/4,ans=0,pos=0;while(l<=r){int mid=(l+r)>>1;int p=check(mid); //二分答案if(p>0){ans=mid;pos=p;l=mid+1;}else{r=mid-1;}}if(ans>ANS){ANS=ans;X=lx[i],Y=ly[pos];}for(auto y:line[i]){ //扫描线更新update(lt,1,H,y,1);update(rt,1,H,y,-1);}}cout<<ANS<<endl;cout<<X<<' '<<Y<<endl;for(int i=1;i<=n;i++)line[x[i]].clear();for(int i=1;i<=tot;i++)tree[i]=0,ls[i]=0,rs[i]=0;return ;
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);int _t=1;cin>>_t;//cout<<fixed<<setprecision(20);for(int i=1;i<=_t;i++){//cout<<"Case "<<i<<": ";solve();}return 0;
}
本题强化我对扫描线应用的理解和线段树二分的应用,对于二维数点问题有了更加深刻的思考。
K - Permutations Harmony
题意:给出 \(n,m\) ,要求构造出一个 \(n*m\) 的矩阵,使得每一行构成一个不重复的排列,每一列求和相对,无法构造输出-1
.
思路:一开始没想着找规律,被硬控了大半小时,后来观察到了性质还是很快就能写完
首先可以发现,每一个排列都有一个对应的排列可以匹配,使得对应位置和相等,那么当 \(n\) 为偶数的时候就很容易去构造方案了。
考虑 \(n\) 为奇数的情况,这里我们想着延续 \(n\) 为偶数可以两两对应的性质,思考如何将多出来了一行处理掉
首先一行肯定不行,所以从三行构造下手,这里我打了深搜的表,打出了如下规律
\(l_1 : 1,2,3,...,m\)
\(l_2:a+1,a+2,a+3,...,m,1,2,...,a\)
\(l_3:2a+1,2a-1,2a-3,...,1,2a,2a-2,...,2\)
这样用三行就可以完成,剩下的两两匹配即可,用Hash去重
不能完成的情况还是比较好考虑的,首先是 \(n>m!\) 的情况,还有 $n=1 && m !=1 $ 的情况
对于 \(n\) 为奇数的情况有 \(n > m!-3\) 是不行的(为了留出三个排列以及对应排列)
实现上要使用 next_permutation
去枚举排列
代码:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define unmap unordered_map
#define unset unordered_set
#define MAXQ priority_queue
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define frep(i,a,b) for(int i=a;i<(b);++i)
using namespace std;
template<typename T> using MINQ=priority_queue<T,vector<T>,greater<T> >;
using pii=pair<int,int>;
using vi=vector<int>;
using vii=vector<vi>;#define maxn 1000101
#define mod 998244353
int n,m,H1,H2,H3;
int fac[maxn],vis[maxn],a[maxn],tot;
int r[maxn];void init(){memset(fac,0x3f,sizeof(fac));fac[0]=1; for(int i=1;i<=10;i++)fac[i]=fac[i-1]*i;
}int Hash(){int h=0;for(int i=1;i<=m;i++)h=(h+r[i]*a[i]%mod)%mod;return h;
}void dfs(int l){if(tot>=n)return;if(l>m){bool flag=0;// for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;int H=Hash();if(H==H1||H==H2||H==H3)flag=1;for(int i=1;i<=m;i++)a[i]=m-a[i]+1;H=Hash();if(H==H1||H==H2||H==H3)flag=1;for(int i=1;i<=m;i++)a[i]=m-a[i]+1;if(flag)return;for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;for(int i=1;i<=m;i++)cout<<m-a[i]+1<<' ';cout<<endl;tot+=2;return;}for(int i=1;i<=m;i++){if(!vis[i]){vis[i]=1;a[l]=i;dfs(l+1);vis[i]=0;a[l]=0;}}
}
void output(){bool flag=0;// for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;int H=Hash();if(H==H1||H==H2||H==H3)flag=1;for(int i=1;i<=m;i++)a[i]=m-a[i]+1;H=Hash();if(H==H1||H==H2||H==H3)flag=1;for(int i=1;i<=m;i++)a[i]=m-a[i]+1;if(flag)return;for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;for(int i=1;i<=m;i++)cout<<m-a[i]+1<<' ';cout<<endl;tot+=2;
}
void permutation(){for(int i=1;i<=m;i++)a[i]=i;while(1){if(tot>=n)break;output();next_permutation(a+1,a+m+1);}
}
void solve(){cin>>m>>n;H1=H2=H3=-1;if(n==1){if(m==1){cout<<"YES\n1\n";return;}else{cout<<"NO\n";return;}}if(fac[m]<n){cout<<"NO"<<endl;return;}if(n%2==0){tot=0;cout<<"YES"<<endl;permutation();return;}if(m%2==0){cout<<"NO"<<endl;return;}if(n>fac[m]-3){cout<<"NO"<<endl;return;}cout<<"YES"<<endl;int p;for(int i=1;i<=m;i++)r[i]=rand()*rand()%mod;p=0;for(int i=1;i<=m;i++)a[++p]=i; H1=Hash();for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;p=0;for(int i=1;i<=m/2+1;i++)a[++p]=m/2+i;for(int i=1;i<=m/2;i++)a[++p]=i; H2=Hash();for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;p=0;for(int i=1;i<=m/2+1;i++)a[++p]=2*(m/2+1)-2*i+1;for(int i=1;i<=m/2;i++)a[++p]=2*(m/2+1)-2*i; H3=Hash();for(int i=1;i<=m;i++)cout<<a[i]<<' ';cout<<endl;tot=3;permutation();
// cout<<H1<<' '<<H2<<' '<<H3<<endl;return ;
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);init();int _t=1;cin>>_t;//cout<<fixed<<setprecision(20);for(int i=1;i<=_t;i++){//cout<<"Case "<<i<<": ";solve();}return 0;
}
D - Elaxia的路线
题意:求无向图中,给定两对点间最短路的最长公共路径。
思路:这题之前我写过,不过那时候的代码被hack了,复盘一下思路
首先一个显然的思路就是将两对点间的最短路的公共边抽出来建成图(非公共边赋权为0保证连通),然后在这个图上去进行一个拓扑排序就可以找到最长公共路径。
但是在建这个新图的时候会有一个问题,我们要将一个无向图建成一个有向图,边如何定向才能保证正确性
具体而言,可以考虑这么一个情况,对于第一个点对所形成的最短路图与第二个点对所形成的最短路图中某一条边的方向不同
一个思路是,以第一个点对最短路图的方向作为基准去定向,只要它在第二个点对的最短路图上即可
但这样的思路是有问题的(就这里之前被hack了)
因为这样可能导致第二点对的最短路图构不成一个路径,也就是同时选中了去的路和回的路
那么如何避免这样的问题呢,只需跑两次分别钦定方向求最大即可。
实现:先对四个目标点跑4次最短路,针对一个方向对每条边判断是否符合条件,加入新图,跑拓扑排序,更新答案。
代码:
#include<bits/stdc++.h>
#define maxn 1000001
#define inf 100000001
using namespace std;
int cnt,cost[maxn],from[maxn],to[maxn],Next[maxn],head[maxn];
int cntq,costq[maxn],fromq[maxn],toq[maxn],Nextq[maxn],headq[maxn];
int rd[maxn],ans[maxn],dis[6][maxn],vis[maxn];
int n,m,X1,Y1,X2,Y2;
queue<int > q;
void add(int x,int y,int z){cnt++;cost[cnt]=z;from[cnt]=x;to[cnt]=y;Next[cnt]=head[x];head[x]=cnt;
}
void addedge(int x,int y,int z){cntq++;costq[cntq]=z;rd[y]++;fromq[cntq]=x;toq[cntq]=y;Nextq[cntq]=headq[x];headq[x]=cntq;
}
void SPFA(int S,int* dis){for(int i=1;i<=n;i++)vis[i]=0,dis[i]=inf;q.push(S);vis[S]=1;dis[S]=0;while(!q.empty()){int u=q.front();q.pop();vis[u]=0;for(int i=head[u];i!=-1;i=Next[i]){int v=to[i];if(dis[v]>dis[u]+cost[i]){dis[v]=dis[u]+cost[i];if(vis[v]==0){vis[v]=1;q.push(v);}}}}
}
void topo(){for(int i=1;i<=n;i++)if(rd[i]==0)q.push(i);while(!q.empty()){int u=q.front();q.pop();for(int i=headq[u];i!=-1;i=Nextq[i]){int v=toq[i];rd[v]--;if(!rd[v])q.push(v);ans[v]=max(ans[v],ans[u]+costq[i]);}}
}
int main(){memset(head,-1,sizeof(head));memset(headq,-1,sizeof(headq));scanf("%d%d",&n,&m);int ANS=0;scanf("%d%d%d%d",&X1,&Y1,&X2,&Y2);for(int i=1;i<=m;i++){int x,y,z;scanf("%d%d%d",&x,&y,&z);add(x,y,z);add(y,x,z);}SPFA(X1,dis[1]);SPFA(Y1,dis[2]);SPFA(X2,dis[3]);SPFA(Y2,dis[4]);for(int i=1;i<=cnt;i++){if(dis[1][from[i]]+cost[i]+dis[2][to[i]]==dis[1][Y1]){if(dis[3][from[i]]+cost[i]+dis[4][to[i]]==dis[3][Y2]){addedge(from[i],to[i],cost[i]);}}}topo();for(int i=1;i<=n;i++)ANS=max(ANS,ans[i]);cntq=0;memset(headq,-1,sizeof(headq));memset(ans,0,sizeof(ans));memset(rd,0,sizeof(rd));for(int i=1;i<=cnt;i++){if(dis[1][from[i]]+cost[i]+dis[2][to[i]]==dis[1][Y1]){if(dis[4][from[i]]+cost[i]+dis[3][to[i]]==dis[3][Y2]){addedge(from[i],to[i],cost[i]);}}}topo();for(int i=1;i<=n;i++)ANS=max(ANS,ans[i]);printf("%d\n",ANS);return 0;
}
F - 炸弹
题意:在一条直线上有 \(n\) 个炸弹,每个炸弹的坐标是 \(x_i\) ,爆炸半径是 \(r_i\) ,当一个炸弹爆炸时,会把半径内炸弹也引爆(会连续引爆)。问,对于每个炸弹求能引爆多少炸弹。
思路:很容易能想到建图去做,再加上区间内连边,想到线段树建图优化,之后想到缩点划去能互相引爆的炸弹
剩下就是维护左右能引爆最远的炸弹编号,这个使用拓扑排序可以很轻易的维护。
实现:这里主要讲线段树建图的实现。线段树建图原理和线段树差不太多,主要是通过连接表示区间的节点去实现减少边数,区间的划分和线段树完全相同,连边由父亲向儿子。
在这之后,如果一个点要连向一个区间的话,可以向线段树区间查找一样找到区间,用边将该点与表示区间的节点们连接即可。
关于本题的做法,我们首先可以用二分求出每个点能第一次引爆的区间,用线段树建图连边
之后就是如思路所说跑tarjan缩点,建新图,在新图上跑拓扑排序,维护最左右能引爆炸弹编号 \(Left 和 Right\) 这两个信息。
最后求得答案。
代码:
#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define unmap unordered_map
#define unset unordered_set
#define MAXQ priority_queue
#define rep(i,a,b) for(int i=a;i<=(b);++i)
#define frep(i,a,b) for(int i=a;i<(b);++i)
using namespace std;
template<typename T> using MINQ=priority_queue<T,vector<T>,greater<T> >;
using pii=pair<int,int>;
using vi=vector<int>;
using vii=vector<vi>;#define mod 1000000007
#define maxn 2000010
#define inf 2e9+10
#define L(node) (node<<1)
#define R(node) (node<<1|1)struct kkk{int l,r;
}tree[maxn];int cnt,from[maxn<<1],to[maxn<<1],Next[maxn<<1],head[maxn];
int scc[maxn],low[maxn],dfn[maxn],Left[maxn],Right[maxn],vis[maxn];
int rev[maxn],in[maxn];
ll a[maxn],r[maxn];
int n,m,times,num;vector<int>e[maxn];void add(int u,int v){cnt++;from[cnt]=u; to[cnt]=v;Next[cnt]=head[u]; head[u]=cnt;
}void addedge(int u,int v){e[u].push_back(v);in[v]++;
}void pushup(int node){tree[node].l=tree[L(node)].l;tree[node].r=tree[R(node)].r;
}void build(int node,int begin,int end){if(begin==end){tree[node].l=tree[node].r=begin;rev[begin]=node;return;}int mid=(begin+end)>>1;build(L(node),begin,mid);build(R(node),mid+1,end);add(node,L(node));add(node,R(node));pushup(node);
}void modify(int node,int begin,int end,int l,int r,int x){if(begin>=l&&end<=r){if(x!=node)add(x,node);return;}int mid=(begin+end)>>1;if(l<=mid)modify(L(node),begin,mid,l,r,x);if(r>mid) modify(R(node),mid+1,end,l,r,x);
}stack<int>s;
void tarjan(int u){dfn[u]=low[u]=++times;s.push(u);vis[u]=1;for(int i=head[u];i!=-1;i=Next[i]){int v=to[i];if(!dfn[v]){tarjan(v);low[u]=min(low[u],low[v]);}else{if(vis[v])low[u]=min(low[u],dfn[v]);}}if(low[u]==dfn[u]){num++;while(1){int x=s.top();scc[x]=num;vis[x]=0;Left[num]=min(Left[num],tree[x].l);Right[num]=max(Right[num],tree[x].r);s.pop();if(x==u)break;}}
}queue<int>q;
void topo(){for(int i=1;i<=num;i++)if(!in[i])q.push(i);while(!q.empty()){int u=q.front();q.pop();for(auto v:e[u]){Left[v]=min(Left[v],Left[u]);Right[v]=max(Right[v],Right[u]);in[v]--; if(!in[v])q.push(v);}}
}void solve(){memset(Left,0x3f,sizeof(Left));memset(head,-1,sizeof(head));cin>>n;for(int i=1;i<=n;i++){cin>>a[i]>>r[i];}build(1,1,n);for(int i=1;i<=n;i++){int L=lower_bound(a+1,a+n+1,a[i]-r[i])-a;int R=upper_bound(a+1,a+n+1,a[i]+r[i])-a-1;modify(1,1,n,L,R,rev[i]);}tarjan(1);for(int i=1;i<=cnt;i++){int u=scc[from[i]],v=scc[to[i]];if(u==v)continue;addedge(v,u);}topo();long long ans=0;for(int i=1;i<=n;i++){ans+=(ll)i*(Right[scc[rev[i]]]-Left[scc[rev[i]]]+1)%mod;ans%=mod;}cout<<ans<<endl;return ;
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);int _t=1;// cin>>_t;//cout<<fixed<<setprecision(20);for(int i=1;i<=_t;i++){//cout<<"Case "<<i<<": ";solve();}return 0;
}
这题还是不是很好调的,当时因为数据范围问题都调了好一阵子
当然这题还有更简单的思路。
我们考虑反着连边,对于一个点 \(i\) ,维护左侧最近能引爆 \(i\) 的点和右侧最近能引爆 \(i\) 的点 \(L,R\) .
这一步可以用单调栈来完成
我们先考虑如何求得左侧能引爆的最小炸弹编号
维护每个炸弹向左能炸到的位置 \(lpos\) ,初始为 \(x_i-r_i\)
我们将所有炸弹存入堆中,每次取出 \(lpos\) 最小的炸弹,更新 \(L,R\) 炸弹
更新完成之后,我们可以用二分去求得 \(lpos\) 炸弹编号
右侧同理,即可求得答案。
然鹅这题还有更nb的 \(O(n)\) 的思路。(暂时还没搞明白)
关于其他题
简要提一下思路
G - 病毒
AC自动机,fail树上dfs
H - 函数求值
化式子后用线段树维护 \(b_i , g_i*b_i , g_i^2*b_i , g_i^3*bi\) 的权值和即可
L - Imbalanced Arrays
这个我们可以想成无向图连边的情况, \(a_i\) 就是每个点的度数,一条边的两个点权值和为正
对于当前 \(n\) 个点,考虑最大权值点,如果它不能连向全部点,则最小点一定不能连向任何点
所以必有度数为 \(n\) 或 \(0\) 的点,且只有一种,否则为NO
接下来按绝对值不断减小去分配这些当前度数为 \(n\) 或 \(0\) 的点即可
M - Not a Nim Problem
推SG函数,发现是个筛法题
O - Greedy Shopping
线段树二分,性质很好
维护区间和,区间最小值,区间最大值(这个好像没用)
对于修改,找到左侧第一个要改的点,然后区间改
对于询问,想找kth那样找就行