ACM寒假第七次集训
ACM寒假第七次集训
Stockbroker Grapevine
思路
这道题类似病毒传播,一传十,十传百。我最先想到的就是广度遍历。当然普通的广度遍历无法直接得出权重不同的图到最远的距离,但是如果我改变进入队列条件,将未曾访问过改为当前这个结点使得访问到下一个结点的时间变短了,我们就将这个更新了状态的结点加入到队列中,毕竟他的更新很可能会影响到其他的结点。至于某一结点是否只访问一次并不重要。这样就能通过bfs的方式得到传播到最远的人需要的时间,然后每个不同的起点都执行一次,用时最少的起点和以这个起点开始遍历到最远需要的时间就是答案。
代码
#include<iostream>
#include <queue>
#include<vector>
#include<climits>
using namespace std;
#define MAX 101
#define vv vector<vector<int> >
#define pii pair<int,int >
void bfs(const vv&graph,vector<int>&ans,int v,int start) {
queue<int> q;
q.push(start);
ans[start]=0;
while (!q.empty()) {
int now=q.front();q.pop();
for (int i=1;i<=v;++i) {
if (i==now) continue;
if (graph[now][i]&&graph[now][i]+ans[now]<ans[i]) {
ans[i]=graph[now][i]+ans[now];
q.push(i);
}
}
}
}
void solve(const int&n) {
vv graph(MAX,vector<int>(MAX,0));//mark可能溢出
int edge;//顶点的数量
for (int i=1;i<=n;++i) {
cin>>edge;
for (int j=1;j<=edge;++j) {
int id,t;
cin>>id>>t;
graph[i][id]=t;
}
}//建立图
//变化的广度优先搜索
pii min_ans=pii(0,INT_MAX);
for (int i=1;i<=n;++i) {
vector<int> ans(MAX,INT_MAX);
bfs(graph,ans,n,i);
int max_ans=INT_MIN;
for (int j=1;j<=n;++j) {
max_ans=max(max_ans,ans[j]);
// cout<<ans[j]<<" ";
}
// cout<<"max_ans="<<max_ans<<endl;
if (min_ans.second>max_ans) {
min_ans={i,max_ans};
}
}
if (min_ans.second<INT_MAX)
cout<<min_ans.first<<" "<<min_ans.second<<endl;
else cout<<"disjoint";
}
int main() {
int n;
while (1) {
cin>>n;
if (n==0) break;
solve(n);
}
return 0;
}
树的直径
思路
已知树的每条边的长度都是一,则可以用bfs广度优先搜索遍历到任意一个点能到达的最远距离,但可惜的是这个长度并不一定是整个树的直径。
但是很容易想到的一个思路是以树的每一个叶子结点为起点,bfs这个树,叶子节点能到的最远距离一定就是树的直径。但是这种算法的时间复杂度非常之高。
任意一个结点第一次bfs遍历到的最远的结点一定是树的直径的某一个端点。然后再以这个点为起点遍历到的最远距离就是树的直径。
问题是:为什么任意一个结点遍历后的端点一定是树直径的端点?
反证法!
(1)起点是直径上的某个点。
假设以这个起点为端点遍历到的最远的点不是树的直径的端点。
那么说明有更长的路径大于树的直径,假设不成立。
(2)起点非树的直径上的点
根据树的性质,这个点一定能联通到树的直径上的某一个点。
假设这个点遍历到的最远点不在树的直径上。不管这个路径会不会过直径上的点,都说明有一段长度比树的直径还长,与题设矛盾,假设不成立。

代码
#include<iostream>
#include <queue>
using namespace std;
#define MAX 100001
#define pii pair<int,int>
struct Edge {
int lp,rp;
struct Edge *lp_next,*rp_next;
Edge(int l=0,int r=0):lp(l),rp(r),lp_next(0),rp_next(0){}
};
struct V {
struct Edge* edges;
}v[MAX];
int n,ans[MAX+1];
int bfs(int start) {
int it=start;
queue<int> q;
bool vis[MAX+1]={false};
q.push(start);
vis[start]=true;
ans[start]=0;
while(!q.empty()) {
int cur=q.front();q.pop();
for (Edge*e=v[cur].edges;e!=0;) {
int nv;
if (e->lp==cur) {
nv=e->rp;e=e->lp_next;
}else if (e->rp==cur) {
nv=e->lp;e=e->rp_next;
}
if (!vis[nv]) {//鑳介亶鍘嗗埌閭d竴瀹氭槸淇濊瘉鏈夎竟鐨?
vis[nv]=true;
ans[nv]=ans[cur]+1;
it=nv;
q.push(nv);
}
}
}
return it;
}
int main() {
cin>>n;
Edge*e=0;
int lp,rp;
for (int i=1;i<n;++i){
cin>>lp>>rp;
// cout<<"lp="<<lp<<" rp="<<rp<<endl;
e=new Edge(lp,rp);
e->lp_next=v[lp].edges;
e->rp_next=v[rp].edges;
v[lp].edges=v[rp].edges=e;
// cout<<i<<endl;
}
cout<<ans[bfs(bfs(1))];
return 0;
}
Invitation Cards
思路
- 构建图结构:我们需要构建两个邻接表,一个用于正向路径(从CCS出发到各个站点),另一个用于反向路径(从各个站点返回CCS)。
- 最短路径计算:使用Dijkstra算法分别计算正向图和反向图的最短路径。正向图的最短路径给出从CCS到各站点的费用,反向图的最短路径给出从各站点返回CCS的费用。
- 总和计算:将每个站点的去程和返程费用相加,得到总费用。
代码
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pii; // {distance, node}
vector<ll> dijkstra(const vector<vector<pii>>& graph, ll start, ll n) {
vector<ll> dist(n + 1, LLONG_MAX);
dist[start] = 0;
priority_queue<pii, vector<pii>, greater<pii>> heap;
heap.push({0, start});
while (!heap.empty()) {
ll current_dist = heap.top().first;
ll u = heap.top().second;
heap.pop();
if (current_dist > dist[u]) continue;
for (const auto& edge : graph[u]) {
ll v = edge.first;
ll w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
heap.push({dist[v], v});
}
}
}
return dist;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll N;
cin >> N;
while (N--) {
ll P, Q;
cin >> P >> Q;
vector<vector<pii>> graph(P + 1);
vector<vector<pii>> reverse_graph(P + 1);
for (ll i = 0; i < Q; ++i) {
ll u, v, w;
cin >> u >> v >> w;
graph[u].push_back({v, w});
reverse_graph[v].push_back({u, w});
}
vector<ll> dist_from_ccs = dijkstra(graph, 1, P);
vector<ll> dist_to_ccs = dijkstra(reverse_graph, 1, P);
ll total_cost = 0;
for (ll i = 2; i <= P; ++i) {
total_cost += dist_from_ccs[i] + dist_to_ccs[i];
}
cout << total_cost << "\n";
}
return 0;
}
战略游戏
思路
我是运用贪心思想。叶子节点的父节点必定被放置士兵,这样,这个父节点周围的所有子节点都会被遍历到,把与这个父节点相关联的边删掉,然后看剩下的图,此时可能整个子树被分成不同的子树部分,任取一个子树的叶子节点,然后删掉与其父节点相关联的所有边视为防止一个士兵,当这个图中没有边的时候视为结束。
代码
#include<iostream>
using namespace std;
#define MAX_V 1501
int n,graph[MAX_V][MAX_V],cnt_d[MAX_V],n_e;
int main() {
cin>>n;
//存边
for (int i=0;i<n;++i) {
int u,v,n_v;
cin>>u>>n_v;
for (int j=0;j<n_v;++j) {
cin>>v;
graph[u][v]=graph[v][u]=1;
++cnt_d[u];
++cnt_d[v];
++n_e;
}
}
//贪心
int ans=0;
while (n_e>0) {
for (int i=0;i<n;++i) {
if (cnt_d[i]==1) {
int u=0;
for (int j=0;j<n;++j)
if (graph[i][j]==1) {u=j;graph[i][j]=0;break;}
for (int j=0;j<n;++j) {
if (graph[u][j]) {
graph[u][j]=0;graph[j][u]=0;
--cnt_d[u];
--cnt_d[j];
--n_e;
}
}
++ans;
break;
}
}
}
cout<<ans<<endl;
return 0;
}
飞行路线
代码
#include<cstdio>
#include<cctype>
#include<cstring>
#include<queue>
#include<algorithm>
#include<vector>
#include<utility>
#include<functional>
int Read()//快速输入
{
int x=0;char c=getchar();
while(!isdigit(c))
{
c=getchar();
}
while(isdigit(c))
{
x=x*10+(c^48);
c=getchar();
}
return x;
}
using std::priority_queue;
using std::pair;
using std::vector;
using std::make_pair;
using std::greater;
struct Edge
{
int to,next,cost;
}edge[2500001];
int cnt,head[110005];
void add_edge(int u,int v,int c=0)
{
edge[++cnt]=(Edge){v,head[u],c};
head[u]=cnt;
}
int dis[110005];
bool vis[110005];
void Dijkstra(int s)
{
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > points;
points.push(make_pair(0,s));
while(!points.empty())
{
int u=points.top().second;
points.pop();
if(!vis[u])
{
vis[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to;
if(dis[to]>dis[u]+edge[i].cost)
{
dis[to]=dis[u]+edge[i].cost;
points.push(make_pair(dis[to],to));
}
}
}
}
}
int main()
{
int n=Read(),m=Read(),k=Read(),s=Read(),t=Read();
int u,v,c;
for(int i=0;i<m;++i)
{
u=Read(),v=Read(),c=Read();
add_edge(u,v,c);
add_edge(v,u,c);
for(int j=1;j<=k;++j)
{
add_edge(u+(j-1)*n,v+j*n);
add_edge(v+(j-1)*n,u+j*n);
add_edge(u+j*n,v+j*n,c);
add_edge(v+j*n,u+j*n,c);
}
}
for(int i=1;i<=k;++i)
{
add_edge(t+(i-1)*n,t+i*n);
}//预防奇葩数据
Dijkstra(s);
printf("%d",dis[t+k*n]);
return 0;
}
二叉苹果树
思路
本题主要是运用树状DP的方法。我们发现这个问题可以逐渐分解为多个相同的子问题。
如果当前的根为cur,要求保留k个树枝,可以左子树保留0,右子树保留k-0;左子树保留1,右子树保留k-1;左子树保留2,右子树保留k-2...(算上直接跟cur连接的那条边)。那么我们就可以求左子树或者右子树,当留下不同个树枝的情况下,最大能够保留多少苹果。
注意这个子树最多能保留他所拥有的树枝个数个树枝,而不能超出这个值,否则就会出现错误
dp[cur] [k] = maxa(dp[cur] [k],dp[child] [j]+dp[cur] [k-1-j]+ info);
dp[cur] [k] 表示以cur为树根,保留k条树枝这个状态下最多保留下的苹果的数量。
对dp[cur] [k-1-j]的理解,为什么这里出现一个cur为根的子树,实际上,当遍历到左子树时,dp[cur] [k-1]还是零,那么这时候存的值必然就是继承原来左子树的值,只是加上info,真正起到作用的是在遍历右子树的时候,表面看是以cur为根,但实际是已经遍历过的左侧在数量范围内的保留不同树枝个数时,苹果的个数,然后此时再看如果开始遍历右边子树后会不会有更好的情况出现。
代码
#include<iostream>
#include<vector>
using namespace std;
#define MAX_NODE 101
#define pii pair<int,int>
int n,max_steps;
vector< vector<pii> > tree(MAX_NODE);
int dp[MAX_NODE][MAX_NODE],size[MAX_NODE];
void DP(int cur,int parent){
for(auto&[child,info]:tree[cur]) {
if (child==parent) continue;
DP(child,cur);size[cur]+=size[child]+1;
for (int k=min(size[cur],max_steps);k>=0;--k) {
for (int j=min(size[child],k-1);j>=0;--j) {
dp[cur][k]=max(dp[cur][k],dp[cur][k-1-j]+dp[child][j]+info);
}
}
}
}
int main(){
cin>>n>>max_steps;
int u,v,info;
while(cin>>u>>v>>info){
tree[u].push_back({v,info});
tree[v].push_back({u,info});
}
DP(1,0);
cout<<dp[1][max_steps]<<endl;
return 0;
}
学习总结
//可重复搜索的bfs
void bfs(const vv&graph,vector<int>&ans,int v,int start) {
queue<int> q;
q.push(start);
ans[start]=0;
while (!q.empty()) {
int now=q.front();q.pop();
for (int i=1;i<=v;++i) {
if (i==now) continue;
if (graph[now][i]&&graph[now][i]+ans[now]<ans[i]) {
ans[i]=graph[now][i]+ans[now];
q.push(i);
}
}
}
}
//用于找到具体某一个顶点的最短路径,但是不一定能够遍历到所有的顶点,并且不能用于有负数权重的情况
void dijkstr(const vv&graph,int v,int start) {//用优先队列优化版本
bool visited[MAX+1]={false};
int dist[MAX+1]={0};
priority_queue<pii> p_q;
for (int i=1;i<=v;++i) {
if (i!=start&&graph[start][i]) {
p_q.push({graph[start][i],i});
dist[i]=graph[start][i];
}
}
visited[start]=true;
while (!p_q.empty()) {
pii nv=p_q.top();p_q.pop();
if (visited[nv.second]) continue;
visited[nv.second]=true;
for (int i=1;i<=v;++i) {
if (!visited[i]&&graph[nv.second][i]<dist[i]) {
dist[i]=graph[nv.second][i];
p_q.push({graph[nv.second][i],i});
}
}
}
}
//树状DP
/*
树状背包通过dfs的方式,后序遍历(左,右,父),遍历到所有的结点,先解决子树的问题(子问题),然后再站在子树问题有解的肩膀上解决当前的问题
*/
#define pii pair<int,int>
vector<vector<pii> > tree(MAX_NODE);
void DP(int cur,int parent){
for(auto&[child,info]:tree[cur]){
if(child==parent)continue;//因为存的是无向图,所以有可能访问到父亲,要直接跳过
DP(child,cur);
//根据状态方程来写
}
}
//如何设置dp[][]的意义,就看每个子问题的状态是什么就好,如果状态用一个维度就可以表示那就用一维,如果是二维的那就表示为二维数组。
//存的这个状态一定是唯一的表示出来子问题的状态。不然答案会出现混乱

浙公网安备 33010602011771号