2025“钉耙编程”中国大学生算法设计暑期联赛(1)
传送门
题目大意
将不同的边划分为不同的协会(颜色)的边,从一个协会(颜色)的边到另一个协会(颜色)的边需要付出一枚金币,一开始使用一个协会(颜色)的边需要一枚金币,求从1号点到n号点的最小花费
题目数据
第一行输入一个整数
T(1≤T≤10),表示测试的总数。
第二行包含两个整数
n 和 m ,(1≤n≤100000, 1≤m≤200000) 表示遗迹数量和传送门数量。
接下来 m 行,每行三个整数 u,v,c ,(1≤u,v≤n, 1≤c≤1000000,1≤c≤1000000) 表示遗迹 u 和 v 之间有一个协会 c的传送门。
保证样例中 ∑n≤200000,∑m≤400000
思路
很明显的分层图思路,但由于点的数量特别大,不能考虑常规分层图建图
而是考虑将同一个集合建成一个图,离散化节点,建立中转节点
最后再建一个超级源点s和超级汇点t,s连接1号遗迹所有子图的点,权值为0, t连接n号遗迹所有子图的点,权值为0。 建完图后跑最短路, 求出 到 的最短距离即为答
案
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
using ll=long long ;
using pii=pair<int,int>;
int cnt,num,tot;
int s,t;
const int maxn=2e6+10;
int dis[maxn];
int head[maxn<<1];
bool book[maxn];
struct node{
int v,next,w;
}e[maxn<<1];
vector<int>p[maxn];
unordered_map<int,int>mp,mp1;
void add(int u,int v,int w){
e[++cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
void dijkstra(){
priority_queue<pii,vector<pii>,greater<pii> >q;
q.push({0,0});
memset(dis,0x3f,sizeof(dis));
memset(book,0,sizeof(book));
dis[0]=0;
while(!q.empty()){
auto [uw,u]=q.top();
q.pop();
book[u]=1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v,w=e[i].w;
if(book[v]) continue;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
return ;
}
void solve(){
int n,m;
cin>>n>>m;
cnt=tot=num=0;
memset(head,0,sizeof(head));
mp.clear();mp1.clear();
for(int i=1;i<=n;++i) p[i].clear();
for(int i=1;i<=m;++i){
int u,v,w;
cin>>u>>v>>w;
if(!mp[w]) mp[w]=++tot;//离散化颜色
int un=u+mp[w]*n,vn=v+mp[w]*n;//同一颜色层
if(!mp1[un]) mp1[un]=++num;//离散化节点
if(!mp1[vn]) mp1[vn]=++num;
add(mp1[un],mp1[vn],0);//同一颜色层之间不需要换色
add(mp1[vn],mp1[un],0);
p[u].push_back(mp1[un]);//u节点在不同颜色层的节点编号
p[v].push_back(mp1[vn]);
}
s=0,t=++num;//源点和汇点
for(int i=1;i<=n;++i){
++num;//中转结点 ,联系同一节点,不同层
for(auto v:p[i]){
add(v,num,0);//进入中转点不需要
add(num,v,1);//从中转点出来相当于换色
if(i==1) add(s,v,1);
if(i==n) add(v,t,0);
}
}
dijkstra();
cout<<dis[t]<<endl;
return ;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int _;
cin>>_;
while(_--){
solve();
}
return 0;
}
中位数
题目大意
给定一个长度为 n 的排列 p[1..n]。
对于所有满足以下条件的区间 [i, j]:
- 1 ≤ i ≤ j ≤ n
- j - i 是偶数(即区间长度 L = j - i + 1是 奇数)
计算:
其中,median(p[i..j]) 是区间 [i, j] 的中位数(即排序后第 (L + 1) / 2 大的数)。
题目数据
- 第一行:整数 n(1 ≤ n ≤ 2000 )。
- 第二行:n 个整数,构成一个排列 p[1..n]。
思路
明显的枚举肯定超时,然而这里会利用到中位数常见处理手法
假设中位数为x,那么设小于x为-1,大于x的为+1,对这样一个序列求和
\(sum[r]=sum[l-1]\)时,区间$ [l,r] $的中位数即为x
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
int n;
using ll=long long ;
const int maxn=2e3+10;
int a[maxn],sum[maxn];
int s[maxn*2];
void solve(){
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
ll ans=0;
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
sum[j]=a[j]>a[i]?1:-1;
}
sum[0]=sum[i]=0;
for(int j=1;j<=n;++j)
sum[j]+=sum[j-1];
memset(s,0,sizeof(s));
for(int j=1;j<=i;++j){
//+2001防止 数组越界
s[sum[j-1]+2001]+=j;对相同的sum[l-1]的下标求和
}
for(int j=i;j<=n;++j){
//此时中位数就为a[i],左端点为满足sum[l-1]=sum[r]的左端点l的和,右端点为j,
ans+=j*s[sum[j]+2001]*a[i];
}
}
cout<<ans<<endl;
}
signed main(){
int t;
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>t;
while(t--){
solve();
}
return 0;
}
子序列
题目大意
给你一个长度为 n 的排列,你需要从中选出一个最长的子序列(注意是子序列不是子串),满足该子序列中两端的值大于中间的所有值。
输出子序列的最大长度。
题目数据
第一行输入一个整数
T (1≤T≤50),表示测试的总数。
对于每个测试用例,第一行输入一个整数 n(1≤n≤2∗10^6)。
接来下一行 n 个整数,表示 1 到 n 的排列。
数据保证 \(Σn≤4×10^6\)
思路
其实是个思维题,从大到小扩展左右边界,就能快速覆盖所有情况
每次扩展时,更新左右端点,区间长度r-l+1,其中无效数字n-i-1(因为这些数都大于i,对答案无贡献)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int t;
int n;
const int maxn=2e6+10;
int a[maxn],f[maxn];
void solve(){
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
f[a[i]]=i;
}
int l,r;
l=r=f[n];
int ans=1;//最小包含自己
for(int i=n-1;i>=1;--i){
l=min(l,f[i]);
r=max(r,f[i]);
//更新后的区间包含所有出现过的数,那么此时比i大的数在区间内对答案无贡献因此减去
ans=max(ans,(r-l+1)-(n-i-1));//若l,r未更新,那么答案已被记录,若更新,计算此区间内的答案
}
cout<<ans<<endl;
return ;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--){
solve();
}
return 0;
}
景区建设
题目大意
在一个n×m的网格区域中,每个格子有唯一高度。游客只能从更高的格子走到相邻(上下左右)的更低格子,且所有格子需能从左上角(1,1)(入口)到达。
若自然路径(按上述规则行走)无法连通,可通过传送器解决:入口已免费配备1个传送器,额外建设每个传送器需花费2³⁴元;两个传送器之间搭建线路的成本为$ 114×|x₁-x₂| + 5141×|y₁-y₂| + 919810×|a_i-a_j| $
目标是让所有格子都能从入口到达,且总花费(传送器费用+线路费用)最少。
题目数据
每个测试包含多个测试用例,具体说明如下:
- 第一行输入测试用例的数量 $ t ( 1 \leq t \leq 20 )$。
- 每个测试用例的格式为:
- 第一行包含两个用空格分隔的数 $ n $ 和 $ m ( 1 \leq n \leq 100, 1 \leq m \leq 100 ) $,分别表示网格的行数和列数。
- 接下来的 $ n $ 行,每行有 $ m $ 个数,其中第 $ i $ 行的第 $ j $ 个数为 $ a_{ij} ( 1 \leq a_{ij} \leq 10000 )$,代表网格中第 $ i $ 行第 $ j $ 列区域的高度。
思路
很明显的我们需要将所有比相邻点都高的点连通,而边权即为\(2^{34}+114×|x₁-x₂| + 5141×|y₁-y₂| + 919810×|a_i-a_j|\)
,所以思路是最小生成树
注意到决定边权相对大小的是919810×|a_i-a_j|,所以利用Kruskal算法,以它为标准排序,就可以求得最小生成树了
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
const int maxn=110;
using ll=long long ;
const ll pr=1ll<<34;
ll h[maxn][maxn];
int cnt=0;
int n,m;
vector<pair<int,int>>s;
void solve(){
cin>>n>>m;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
cin>>h[i][j];
s.clear();
s.push_back({1,1});
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
if(i==1 && j==1 ) continue;
if(h[i][j]>max({h[i-1][j],h[i+1][j],h[i][j+1],h[i][j-1]})) s.push_back({i,j});
}
sort(s.begin(),s.end(),[&](const pair<int,int>& p1,const pair<int,int>&p2){
return h[p1.first][p1.second]<h[p2.first][p2.second];
});
// cout<<s.size()<<endl;
ll ans=0;
for(int i=1;i<s.size();i++){
auto [x1,y1]=s[i];
auto [x2,y2]=s[i-1];
ll w=114ll*abs(x1-x2)+5141ll*abs(y1-y2)+919810ll*abs(h[x1][y1]-h[x2][y2]);
ans+=w;
}
cout<<ans+1ll*(s.size()-1)*pr<<endl;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T = 1;
cin >> T;
while(T--){
solve();
}
}
树上LCM
题目大意
给你一棵由 n 个节点的树和一个数 x,其中每个节点都有一个值。有多少条简单路径的值的 lcm 为 x?
一条简单路径的 lcm 的定义为路径上所有节点的值的lcm。
题目数据

思路
官方思路
LCM重要结论
- LCM(x, y) 的过程实际上是对 x, y 的每个质因子个数取max的过程。
- 如果 LCM(x, y)!=x,那么如果仅对 y 进行 LCM 操作是无法变成 x 的
首先将x质因数分解,其中质因数分别为\(a_0,a_1,a_2,....\),个数分别为\(b_1,b_2,b_3,......\),(注意到\(10^7\),不同的质因数不超过7个),定义状态二进制位数,某个数具有相同的质因数个数,则该位为1,例如某个数的状态表示为0011,则说明该数具有\(a_0,a_1\)质因数且个数分别为\(b_1,b_2\)
接下来就是树形dp
dp[u][mask] 表示:以节点 u 为终点的所有简单路径中,路径上所有节点值的编码组合为 mask 的路径总数
状态转移对于状态i,dp[u][i|a[u]]+=dp[v][i],所有i状态的路径和a[u]异或(a[u]表示u节点值状态压缩后的值)
对于u的一个子节点v,他所具有的路径条数可以与其他u的子节点相乘,而快速计算其他所有路径的和,用前缀和预先处理
评测很莫名奇妙,清空vector数组会超时
点击查看代码
#include<bits/stdc++.h>
#pragma GCC optimize(3,"Ofast","inline")
#pragma GCC optimize(2)
using namespace std;
using ll=long long ;
#define endl "\n"
map<int,int>mp;
int n,x;
const int maxn=1e5+10;
int a[maxn];
vector<vector<int> >g(maxn);
ll ans=0;
inline int get(int y){//得到x因数组成的二进制对应下的二进制
if(lcm(x,y)!=x) return -1;
int val=0,j=0;
for(auto [v,c]:mp){
int cur=0;
while(y%v == 0 ){
y/=v;
++cur;
}
if(cur==c) val|=(1<<j);
++j;
}
return val;
}
inline void dfs(int u,int fa,int k,vector<vector<int> >&f){
for(auto v:g[u]){
if(v==fa) continue;
dfs(v,u,k,f);
}
if(a[u]==-1) return ;//必然不可能经过它达到题目要求
f[u][a[u]]=1;
for(auto v:g[u]){
if(v==fa) continue;
auto s=f[u];
//状态的前缀和,0越多的越在后面,这样保证求的1的前缀和 包括了不需要1的位置的状态和需要1的位置的状态
for (int j = 0; j < k; j++) {
for (int i = 0; i < (1 << k); i++) {
if (!(i >> j & 1)) {
s[i] += s[i ^ (1 << j)];
}
}
}
for(int i=0;i<(1<<k);++i){
int y=i|a[u];//状态i和a[u]有的相同的因数个数的状态
int need=y^((1<<k)-1);//lcm等于x需要的因数
ans+=1ll*s[need]*f[v][i];
f[u][y]+=f[v][i];
}
}
if(a[u]==(1<<k)-1) ++ans;
return ;
}
inline void solve(){
cin>>n>>x;
ans=0;
mp.clear();
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
//分解x质因数
int nx=x;
for(int i=2;i*i<=nx;++i) {
int cnt=0;
while(nx%i==0){
nx/=i;
++cnt;
}
if(cnt) mp[i]=cnt;
}
if(nx>1) mp[nx]=1;
for(int i=1;i<=n;++i){
int y;cin>>y;
a[i]=get(y);
}
const int k=(int)mp.size();
vector<vector<int> >f(n+1,vector<int>(1<<k));
//以节点 u 为终点的所有简单路径中,路径上所有节点值的编码组合为 mask 的路径总数。
dfs(1,0,k,f);
cout<<ans<<endl;
for(int i=1;i<n;++i) g[i].clear();
}
int main(){
ios::sync_with_stdio(0);
cin.tie(nullptr);
cout.tie(0);
int _=1;
cin>>_;
while(_--){
solve();
}
return 0;
}

浙公网安备 33010602011771号