ACM寒假集训第七次专题任务

ACM寒假集训第七次专题任务

一、Stockbroker Grapevine

题目:

联想截图_20250218180856联想截图_20250218180908

解题思路:

通过 Floyd-Warshall 算法计算所有股票经纪人之间的最短传播时间,从而找到一个最佳起点,使得从该起点传播消息到所有其他经纪人的最长时间最小。如果图是不连通的(即某些经纪人无法通过任何路径联系到),则输出“disjoint”。

AC代码:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn=105;
const int inf=0x3f3f3f3f;
int main()
{
	while(1)
	{
		int n;
		cin>>n;
		if(n==0)
		{
			break;
		}
		vector<vector<int>> dist;
		dist.resize(n+1);
		for(int i=0;i<=n;i++)
		{
			dist[i].resize(n+1);
		}
		for(int i=0;i<=n;i++)
		{
			for(int j=0;j<=n;j++)
			{
				dist[i][j]=inf;
			}
		}
		for(int i=1;i<=n;i++)
		{
			dist[i][i]=0;
		}
		for(int i=1;i<=n;i++)
		{
			int m;
			cin>>m;
			while(m--)
			{
				int to,cost;
				cin>>to>>cost;
				dist[i][to]=cost;
			}
		}
		for(int k=1;k<=n;k++)
		{
			for(int i=1;i<=n;i++)
			{
				for(int j=1;j<=n;j++)
				{
					if(dist[i][k]!=inf&&dist[k][j]!=inf)
					{
						dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
					}
				}
			}
		}
		int best_start=-1;
		int min_max_time=inf;
		for(int i=1;i<=n;i++)
		{
			int max_time=0;
			bool all_r=true;
			for(int j=1;j<=n;j++)
			{
				if(dist[i][j]==inf)
				{
					all_r=false;
					break;
				}
				max_time=max(max_time,dist[i][j]);
			}
			if(all_r&&max_time<min_max_time)
			{
				min_max_time=max_time;
				best_start=i;
			}
		}
		if(best_start==-1)
		{
			cout<<"disjoint"<<endl;
		}
		else
		{
			cout<<best_start<<" "<<min_max_time<<endl;
		}
	}
	return 0;
}

二、树的直径

题目:

联想截图_20250218181005联想截图_20250218181022

解题思路:

第一次 BFS 从任意节点出发,找到距离最远的节点 A。这个节点 A 一定是直径的一个端点。

第二次 BFS 从 A 出发,找到距离 A 最远的节点 B。节点 A 到 B 的路径即为树的直径。

运用了树的性质:从任意节点出发,最远的节点一定是直径的一个端点。

AC代码:

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int maxn=100009;
vector<int> adj[maxn];
int dist[maxn];
bool visited[maxn];
int n;
void bfs(int start,int &farthestnode,int &maxdist)
{
	queue<int> q;
	q.push(start);
	visited[start]=true;
	dist[start]=0;
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int v:adj[u])
		{
			if(!visited[v])
			{
				visited[v]=true;
				dist[v]=dist[u]+1;
				q.push(v);
			}
		}
	}
	maxdist=0;
	farthestnode=start;
	for(int i=1;i<=n;i++)
	{
		if(dist[i]>maxdist)
		{
			maxdist=dist[i];
			farthestnode=i;
		}
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<n;i++)
	{
		int u,v;
		cin>>u>>v;
		adj[u].push_back(v);
		adj[v].push_back(u);
	}
	int farthestnode1,maxdist1;
	bfs(1,farthestnode1,maxdist1);
	for(int i=1;i<=n;i++)
	{
		visited[i]=false;
	}
	int farthestnode2,maxdist2;
	bfs(farthestnode1,farthestnode2,maxdist2);
	cout<<maxdist2;
	return 0;
}

三、Invitation Cards

题目:

联想截图_20250218181127联想截图_20250218181143

解题思路:

使用邻接表存储正向图(从出发站到目的站)和反向图(从目的站到出发站)。正向图用于计算从CCS到每个站点的最短路径;反向图用于计算从每个站点返回CCS的最短路径。

对于每个测试用例,分别从CCS(站点1)出发,使用Dijkstra算法计算:

distto[i]:从CCS到站点i的最短路径。

distfrom[i]:从站点i返回CCS的最短路径。

对于每个站点i,计算其往返总费用:distto[i] + distfrom[i]。将所有站点的往返费用相加,即为最终的最小交通费用。

AC代码:

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
#include<functional>
using namespace std;
const int maxn=1000009;
const int inf=0x3f3f3f3f;
struct edge{
	int to;
	int cost;
};
vector<edge> graph[maxn];
vector<edge> rgraph[maxn];
int distto[maxn];
int distfrom[maxn];
void dijkstra(int start,int dist[],vector<edge> graph[])
{
	priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> pq;
	pq.push({0,start});
	dist[start]=0;
	while(!pq.empty())
	{
		int u=pq.top().second;
		int d=pq.top().first;
		pq.pop();
		if(d>dist[u])
		{
			continue;
		}
		for(auto &e:graph[u])
		{
			int v=e.to;
			int cost=e.cost;
			if(dist[v]>dist[u]+cost)
			{
				dist[v]=dist[u]+cost;
				pq.push({dist[v],v});
			}
		}
	}
}
int main()
{
	int N;
	cin>>N;
	while(N--)
	{
		int P,Q;
		cin>>P>>Q;
		for(int i=1;i<=P;i++)
		{
			graph[i].clear();
			rgraph[i].clear();
		}
		for(int i=0;i<Q;i++)
		{
			int u,v,cost;
			cin>>u>>v>>cost;
			graph[u].push_back({v,cost});
			rgraph[v].push_back({u,cost});
		}
		fill(distto+1,distto+P+1,inf);
		fill(distfrom+1,distfrom+P+1,inf);
		dijkstra(1,distto,graph);
		dijkstra(1,distfrom,rgraph);
		long long sum=0;
		for(int i=2;i<=P;i++)
		{
			sum+=distto[i]+distfrom[i];
		}
		cout<<sum<<endl;
	}
}

四、战略游戏

题目:

联想截图_20250218181237联想截图_20250218181258

解题思路:

定义 dp[u][0]:表示节点 u 不放置士兵时,覆盖该子树所需的最少士兵数。

定义 dp[u][1]:表示节点 u 放置士兵时,覆盖该子树所需的最少士兵数。

状态转移方程:如果节点 u 不放置士兵(dp[u][0])则至少有一个子节点必须放置士兵;如果节点 u 放置士兵(dp[u][1])则所有子节点可以选择放置或不放置士兵。

使用DFS计算每个节点的两种状态(放置和不放置士兵)。

AC代码:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn=1509;
vector<int> tree[maxn];
int dp[maxn][2];
void dfs(int u,int parent)
{
	dp[u][0]=0;
	dp[u][1]=1;
	for(int v:tree[u])
	{
		if(v==parent)
		{
			continue;
		}
		dfs(v,u);
		dp[u][0]+=dp[v][1];
		dp[u][1]+=min(dp[v][0],dp[v][1]);
	}
}
int main()
{
	int n;
	cin>>n;
	for(int i=0;i<n;i++)
	{
		int id,k;
		cin>>id>>k;
		while(k--)
		{
			int r;
			cin>>r;
			tree[id].push_back(r);
			tree[r].push_back(id);
		}
	}
	dfs(0,-1);
	cout<<min(dp[0][0],dp[0][1]);
	return 0;
}

五、飞行路线

题目:

联想截图_20250218181428联想截图_20250218181455

解题思路:

求从起点城市 s 到终点城市 t 的最少花费,但允许在最多 k 次航线上免费乘坐。这意味着:

每次免费乘坐可以节省一次飞行的费用。

免费乘坐次数用完后,后续的飞行需要支付正常费用。

对于每个状态 (u,r,cost),考虑两种转移方式:

  • 免费乘坐:如果 r>0,可以选择免费乘坐到相邻节点 v,状态变为 (v,r−1,cost)。
  • 付费乘坐:可以选择付费乘坐到相邻节点 v,状态变为 (v,r,cost+c),其中 c 是从 u 到 v 的费用。

AC代码:

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=10005;
const int inf=0x3f3f3f3f;
struct edge{
	int to;
	int cost;
};
vector<edge> graph[maxn];
int dist[maxn][11];
void dijkstra(int s,int t,int k)
{
	priority_queue<pair<int,pair<int,int>>,vector<pair<int,pair<int,int>>>,greater<pair<int,pair<int,int>>>> pq;
	pq.push({0,{s,k}});
	dist[s][k]=0;
	while(!pq.empty())
	{
		int cost=pq.top().first;
		int u=pq.top().second.first;
		int r=pq.top().second.second;
		pq.pop();
		if(u==t)
		{
			continue;
		}
		for(auto &e:graph[u])
		{
			int v=e.to;
			int c=e.cost;
			if(r>0&&dist[v][r-1]>cost)
			{
				dist[v][r-1]=cost;
				pq.push({cost,{v,r-1}});
			}
			if(dist[v][r]>cost+c)
			{
				dist[v][r]=cost+c;
				pq.push({cost+c,{v,r}});
			}
		}	
	}
}
int main()
{
	int n,m,k;
	cin>>n>>m>>k;
	int s,t;
	cin>>s>>t;
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		graph[a].push_back({b,c});
		graph[b].push_back({a,c});
	}
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<=k;j++)
		{
			dist[i][j]=inf;
		}
	}
	dijkstra(s,t,k);
	int minc=inf;
	for(int r=0;r<=k;r++)
	{
		minc=min(minc,dist[t][r]);
	}
	cout<<minc;
	return 0;
}

六、二叉苹果树

题目:

联想截图_20250218181644联想截图_20250218181659

解题思路:

使用邻接表 son 存储每个节点的子节点,二维数组 val 存储每根树枝上的苹果数量。f[x][j] 表示以节点 x 为根,保留 j 根树枝时,最多能保留的苹果数量。

从根节点(节点 1)开始,递归遍历每个节点的子节点。对于每个节点 x,考虑其所有子节点 ny,更新动态规划数组 f

对于每个节点 x,假设保留 j 根树枝,考虑其子节点 ny:如果保留 k 根树枝在子节点 ny 上,则剩余 j−k−1 根树枝可以分配给其他子节点。更新 f[x][j] 的值,考虑当前子节点 ny 的苹果数量 val[x][ny],以及子节点 ny 的最优解 f[ny][k]

AC代码:

#include<iostream>
#include<vector>
using namespace std;
vector<int> son[109];
int n,q;
int f[109][109];
int val[109][109];
int used[109];
void dfs(int x)
{
	used[x]=1;
	for(int i=0;i<son[x].size();i++)
	{
		int ny=son[x][i];
		if(used[ny]==1)
		{
			continue;
		}
		used[ny]=1;
		dfs(ny);
		for(int j=q;j>=1;j--)
		{
			for(int k=j-1;k>=0;k--)
			{
				f[x][j]=max(f[x][j],val[x][ny]+f[ny][k]+f[x][j-k-1]);
			}
		}
	}
}
int main()
{
	cin>>n>>q;
	for(int i=1;i<n;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		val[a][b]=c;
		val[b][a]=c;
		son[a].push_back(b);
		son[b].push_back(a);
	}
	dfs(1);
	cout<<f[1][q];
	return 0;
}

学习总结

一、图与树的基本概念

(一)图(Graph)

  1. 定义
    图是由顶点集合(Vertex Set)和边集合(Edge Set)组成的结构,记作 G=(V,E)。顶点之间通过边相连,边可以是有向的(有向图),也可以是无向的(无向图)。
  2. 分类
    • 无向图:边没有方向,如果顶点 u 和 v 之间有边,则 u 和 v 是相互连通的。
    • 有向图:边有方向,从一个顶点指向另一个顶点。如果从 u 到 v 有一条边,但不一定从 v 到 u 有边。
    • 带权图:每条边都有一个权重(权值),表示边的“代价”或“距离”。
  3. 存储方式
    • 邻接矩阵:用一个二维数组$ adj[n][n] $表示,其中 n 是顶点数。如果顶点 u 和 v 之间有边,则 \(adj[u][v]=1\)(无权图)或 \(adj[u][v]=weight\)(带权图)。优点是方便判断两点之间是否有边,缺点是空间复杂度高(O(n2))。
    • 邻接表:用一个数组存储每个顶点的邻接点列表。对于每个顶点 u,存储所有与 u 直接相连的顶点。优点是空间复杂度低(O(n+m),其中 m 是边数),适合稀疏图。

(二)树(Tree)

  1. 定义
    树是一种特殊的图,它是无向图的连通无环子图。树具有以下性质:
    • 任意两个顶点之间有且仅有一条路径。
    • 如果树有 n 个顶点,则一定有 n−1 条边。
  2. 分类
    • 无根树:没有明确的根节点,任意一个顶点都可以作为根。
    • 有根树:有一个明确的根节点,树上的每个顶点都有一个唯一的父节点(根节点除外)。
  3. 存储方式
    • 邻接表:与图的邻接表类似,存储每个节点的子节点。
    • 父节点数组:用一个数组 parent[n] 表示每个节点的父节点。

二、图的存储与建图

(一)存储方式

  1. 邻接矩阵

    const int MAXN = 100; // 最大顶点数
    int adj[MAXN][MAXN]; // 邻接矩阵
    

    初始化:无权图用 0 和 1 表示,带权图用无穷大(如 INT_MAX)表示无边。

  2. 邻接表

    const int MAXN = 100; // 最大顶点数
    const int MAXM = 200; // 最大边数
    vector<int> adj[MAXN]; // 邻接表
    

    或者使用链式前向星存储边:

    struct Edge {
        int to, weight;
    } edges[MAXM];
    int head[MAXN], tot = 0; // head数组存储每个点的头节点,tot是边的编号
    void addEdge(int from, int to, int weight) {
        edges[tot] = {to, weight};
        edges[tot].next = head[from];
        head[from] = tot++;
    }
    

(二)建图

  1. 无向图

    void addEdge(int u, int v, int w) {
        adj[u][v] = w;
        adj[v][u] = w; // 无向图需要双向存储
    }
    

    或者邻接表:

    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u); // 无向图需要双向存储
    }
    
  2. 有向图

    void addEdge(int u, int v, int w) {
        adj[u][v] = w;
    }
    

    或者邻接表:

    void addEdge(int u, int v) {
        adj[u].push_back(v);
    }
    

三、树的遍历

(一)深度优先搜索(DFS)

  1. 递归实现

    void dfs(int u, int parent) {
        for (int v : adj[u]) 
        {
            if (v == parent) continue; // 避免回溯
            dfs(v, u);
        }
    }
    
  2. 非递归实现(用栈)

    stack<int> s;
    vector<bool> visited(n, false);
    s.push(root);
    while (!s.empty())
    {
        int u = s.top();
        s.pop();
        if (!visited[u])
        {
            visited[u] = true;
            for (int v : adj[u]) 
            {
                if (!visited[v]) 
                {
                    s.push(v);
                }
            }
        }
    }
    

(二)广度优先搜索(BFS)

  1. 队列实现

    queue<int> q;
    vector<bool> visited(n, false);
    q.push(root);
    visited[root] = true;
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        for (int v : adj[u])
        {
            if (!visited[v]) 
            {
                visited[v] = true;
                q.push(v);
            }
        }
    }
    

四、最短路算法

(一)Dijkstra算法

  1. 适用范围
    适用于非负权图,求单源最短路径。

  2. 算法步骤

    • 初始化:所有顶点到源点的距离为无穷大,源点到自己的距离为0。
    • 使用优先队列(小根堆)存储顶点,按距离从小到大排序。
    • 每次取出距离最小的顶点 u,更新其邻接点 v 的距离。
  3. 代码实现

    #include <queue>
    #include <vector>
    #include <climits>
    using namespace std;
    
    vector<vector<pair<int, int>>> adj; // 邻接表
    vector<int> dijkstra(int start, int n) {
        vector<int> dist(n, INT_MAX);
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
        dist[start] = 0;
        pq.push({0, start});
    
        while (!pq.empty())
        {
            int u = pq.top().second;
            int d = pq.top().first;
            pq.pop();
    
            if (d > dist[u]) continue; // 剪枝
    
            for (auto [v, w] : adj[u])
            {
                if (dist[u] + w < dist[v])
                {
                    dist[v] = dist[u] + w;
                    pq.push({dist[v], v});
                }
            }
        }
        return dist;
    }
    

(二)Floyd算法

  1. 适用范围
    适用于所有类型的图(包括负权边,但不能有负权环),求多源最短路径。

  2. 算法步骤

    • 初始化邻接矩阵,设 \(dist[i][j]\) 为顶点 i 到 j 的最短距离。

    • 通过三重循环动态更新最短距离:

      for (int k = 0; k < n; k++) 
      {
          for (int i = 0; i < n; i++) 
          {
              for (int j = 0; j < n; j++)
              {
                  if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX)
                  {
                      dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                  }
              }
          }
      }
      
  3. 代码实现

    const int INF = INT_MAX;
    void floyd(int n) {
        for (int k = 0; k < n; k++) 
        {
            for (int i = 0; i < n; i++) 
            {
                for (int j = 0; j < n; j++) 
                {
                    if (dist[i][k] != INF && dist[k][j] != INF) 
                    {
                        dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                    }
                }
            }
        }
    }
    

五、树上问题

(一)DFS序

  1. 定义
    DFS序是通过深度优先搜索遍历树时,访问每个节点的顺序。可以用于将树上的问题转化为序列问题。

  2. 实现

    vector<int> dfsOrder;
    void dfs(int u, int parent) {
        dfsOrder.push_back(u);
        for (int v : adj[u]) 
        {
            if (v == parent) continue;
            dfs(v, u);
        }
    }
    

(二)简单树形DP(例:没有上司的舞会)

  1. 问题描述
    一个公司有 n 个员工,每个员工有一个快乐值。每个员工可以邀请或不邀请,但不能同时邀请一个员工和他的直接上司。目标是最大化邀请员工的总快乐值。

  2. 解题思路

    • 使用树形DP,定义 \(dp[u][0]\) 表示不邀请节点 u 的最大快乐值,\(dp[u][1]\) 表示邀请节点 u 的最大快乐值。
    • 状态转移方程:
      • \(dp[u][0]=∑max(dp[v][0],dp[v][1])\)(不邀请 u,子节点可以自由选择)
      • \(dp[u][1]=joy[u]+∑dp[v][0]\)(邀请 u,子节点不能邀请)
  3. 代码实现

    vector<vector<int>> adj;
    vector<int> joy;
    vector<vector<int>> dp;
    
    void dfs(int u, int parent) {
        dp[u][1] = joy[u]; // 邀请当前节点
        for (int v : adj[u]) 
        {
            if (v == parent) continue;
            dfs(v, u);
            dp[u][0] += max(dp[v][0], dp[v][1]); // 不邀请当前节点
            dp[u][1] += dp[v][0]; // 邀请当前节点,子节点不能邀请
        }
    }
    
posted @ 2025-02-19 23:10  cytlllll  阅读(20)  评论(0)    收藏  举报