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是 奇数

计算:

\[\sum_{\substack{1 \le i \le j \le n \\ j - i \text{ 为偶数}}} i \cdot j \cdot \mathrm{median}(p[i..j]) \]

其中,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| $

目标是让所有格子都能从入口到达,且总花费(传送器费用+线路费用)最少。

题目数据

每个测试包含多个测试用例,具体说明如下:

  1. 第一行输入测试用例的数量 $ t ( 1 \leq t \leq 20 )$。
  2. 每个测试用例的格式为:
    • 第一行包含两个用空格分隔的数 $ 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。

题目数据

image

思路

官方思路

LCM重要结论

  1. LCM(x, y) 的过程实际上是对 x, y 的每个质因子个数取max的过程。
  2. 如果 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;
}
posted @ 2025-08-20 10:32  归游  阅读(59)  评论(0)    收藏  举报