2020杭电多校暑期集训
图论个人总结
Day 4:
补题的时候越发觉得杭电的图论题出的非常好,也是是我做的比较少
这是最近补的两题,比较菜,比赛的时候都没出,补完之后真的觉得题非常好
题意: 有n个节点、m条边构成的无向图,每个节点有特殊的要求,L只能用左手,R只能用右手,M都可以,左右换手需要额外的时间,让你求从s到t花费的最小时间。
比赛的时候觉得这是一道很裸的迪杰斯特拉,但是难点在于当前左右手的状态影响到下一次路径的距离,当时觉得在建图的时候可以把换手的时间算进权值里,但还是不好处理。题解就非常的妙,非常的妙。
讲每个节点拆分成两个点s1,s2,s1表示当前左手经过该节点,s2表示右手经过该节点,则可以对s1,s2分别建边,若该节点只能左手通过,则s2不存在,同理可建立一张新图,但是,这时候要至少跑4边Dijkstra才能得到最短路,因为不知道s、t的左右手状态。==这时候,我们将S0连接s1、s2,S(2n+1)连接t1、t2,计算从S0到S(2n+1)==就可以得到所求的最短路径了。
这是真的妙啊,应该是我菜,想不到,希望以后就会了
贴一下代码
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=1e6+5; //注意这里一定要开大一点,不然RE到吐
const ll mod=1e9+7;
typedef pair<ll,ll> P;
ll n,m,ss,t,x;
int fir[maxn],nex[maxn],to[maxn],cnt;
ll val[maxn];
char s[maxn];
void add_edge(int u,int v,ll w){ //前向星建图
nex[++cnt]=fir[u];
fir[u]=cnt;
to[cnt]=v;
val[cnt]=w;
}
struct node{ //堆优化的Dijkstra
int idex;
ll dis;
node(){}
node(int idex,ll dis) : idex(idex),dis(dis){}
bool operator <(const node &rhs) const
{
return dis>rhs.dis;
}
};
ll dis[maxn];
int vis[maxn];
void Dijkstra(int s){
for(int i=0;i<=2*n+1;i++){
vis[i]=0;
dis[i]=inf;
}
dis[s]=0;
priority_queue<node> q;
q.push(node(s,0));
while(!q.empty()){
node u=q.top();
q.pop();
if(vis[u.idex]==1) continue;
vis[u.idex]=1;
for(int e=fir[u.idex];e;e=nex[e]){
int v=to[e];
ll w=val[e];
if(u.dis+w<dis[v]){
dis[v]=u.dis+w;
q.push(node(v,dis[v]));
}
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--){
scanf("%lld %lld %lld %lld %lld",&n,&m,&ss,&t,&x);
scanf(" %s",s);
// cout<<s<<'\n';
for(int i=0;i<=cnt;i++){
nex[i]=fir[i]=to[i]=val[i]=0;
}
cnt=0;
while(m--){
ll u,v,d;
scanf("%lld %lld %lld",&u,&v,&d);
if(s[u-1]=='L'&&s[v-1]=='L'){ //都是左手 花费为d
add_edge(u,v,d);
add_edge(v,u,d);
}
else if(s[u-1]=='L'&&s[v-1]=='R'){ //一左一右 花费d+x
add_edge(u,n+v,d+x);
add_edge(n+v,u,d+x);
}
else if(s[u-1]=='L'&&s[v-1]=='M'){
add_edge(u,v,d);
add_edge(v,u,d);
add_edge(u,n+v,d+x);
add_edge(n+v,u,d+x);
}
else if(s[u-1]=='R'&&s[v-1]=='R'){
add_edge(n+u,v+n,d);
add_edge(v+n,n+u,d);
}
else if(s[u-1]=='R'&&s[v-1]=='L'){
add_edge(n+u,v,d+x);
add_edge(v,n+u,d+x);
}
else if(s[u-1]=='R'&&s[v-1]=='M'){
add_edge(u+n,v,d+x);
add_edge(v,u+n,d+x);
add_edge(u+n,n+v,d);
add_edge(n+v,u+n,d);
}
else if(s[u-1]=='M'&&s[v-1]=='L'){
add_edge(u,v,d);
add_edge(v,u,d);
add_edge(u+n,v,d+x);
add_edge(v,u+n,d+x);
}
else if(s[u-1]=='M'&&s[v-1]=='R'){
add_edge(u,v+n,d+x);
add_edge(v+n,u,d+x);
add_edge(u+n,v+n,d);
add_edge(v+n,u+n,d);
}
else if(s[u-1]=='M'&&s[v-1]=='M'){
add_edge(u,v,d);
add_edge(v,u,d);
add_edge(u+n,v,d+x);
add_edge(v,u+n,d+x);
add_edge(u,v+n,d+x);
add_edge(v+n,u,d+x);
add_edge(u+n,v+n,d);
add_edge(v+n,u+n,d);
}
}
add_edge(0,ss,0); //连接起点和。第一个点
add_edge(ss,0,0);
add_edge(0,n+ss,0);
add_edge(n+ss,0,0);
add_edge(2*n+1,t,0);//连接终点和最后一个点
add_edge(t,2*n+1,0);
add_edge(t+n,2*n+1,0);
add_edge(2*n+1,t+n,0);
Dijkstra(0);
printf("%lld\n",dis[2*n+1]);
}
return 0;
}
Day 6:
这题比赛的时候想到跟最小生成树有关,但后续处理没有想到,再加上卡在01题,也是赛后补的。有一说一,对dfs还是要多写!多写!多写!
题目传送门
题意:给你一个n个节点m条边的无向图,题目要求所有白点和黑点最短路的值。 每条边的权值是2^i,由简单数学可知,所以如果两个点能通过前i-1条边到达,那肯定比通过第i条更优,所以我们从1到n按顺序建最小生成树。
但怎么计算每条边的贡献?一开始我也万脸懵逼,看到这句话才知道,建立最小生成树,树上的所有边对答案都是有贡献的,现在考虑单边对答案的贡献。我们可以用一遍DFS处理出每条边左边的1号点个数、右边的0号点个数,两者相乘再乘上这条边的权值,左右相反同理,这样就可以算出单边对答案的贡献。 这应该就是解这道题目的关键了。说实话,之前只会克鲁斯卡尔的裸题,这次算是学到了。希望下次比赛也能想到!
详细可见代码注释:
#include<iostream>
#include<vector>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<set>
#include<algorithm>
#include<queue>
#include<stack>
#include<list>
#include<map>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=1e5+5;
const ll mod=1e9+7;
typedef pair<ll,ll> P;
int a[maxn],pre[maxn];
int num1,num0;//总0、1数,方便后面容斥
ll dp[maxn][2],ans;
vector<P> G[maxn];
void init(int n){ //基本裸的克鲁斯卡尔
for(int i=1;i<=n;i++){
pre[i]=i;
}
}
int find(int x){
if (pre[x] == x ) return x;
else return pre[x]=find(pre[x]);
}
void dfs(int u,int pre){
dp[u][0]=dp[u][1]=0;//dp[i][0]表示该节点后连着的0,dp[i][1]表示该节点后连着的1
dp[u][a[u]]++;//加这个节点
for(auto it=G[u].begin();it!=G[u].end();it++){
int v=it->first;
if(v==pre) continue; //这一步很重要,不然会将该节点前的0、1也计入
dfs(v,u); //得到下一节点v的0、1数
dp[u][0]+=dp[v][0];
dp[u][1]+=dp[v][1];
}
for(auto it=G[u].begin();it!=G[u].end();it++){
int v=it->first;
if(v==pre) continue; //不能少,否则重复计算
ans=(ans+(1ll*(num1-dp[v][1])*dp[v][0]%mod)*it->second%mod)%mod;
ans=(ans+(1ll*(num0-dp[v][0])*dp[v][1]%mod)*it->second%mod)%mod;
//核心步骤 其中num1-dp[v][1]表示该节点之前的1,num0-dp[v][0]表示该节点前的1
//容斥的思想,妙啊
}
}
int main()
{
int T;
cin>>T;
while(T--){
int n,m;
scanf("%d %d",&n,&m);
init(n);
num1=num0=0;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
dp[i][0]=dp[i][1]=0;
G[i].clear();
if(a[i]==1) num1++;
else num0++;
}
ll w=1;
ans=0;
for(int i=1;i<=m;i++){
w=w*2%mod;
int u,v;
scanf("%d %d",&u,&v);
int fx=find(u),fy=find(v);
if(fx!=fy){ //克鲁斯卡尔建图
G[u].push_back({v,w});
G[v].push_back({u,w});
pre[fx]=fy;
}
}
dfs(1,-1);
printf("%lld\n",ans%mod);
}
return 0;
}
个人比较菜,能补的题补的都挺慢,接下来杭电的题会坚持补,如果有问题欢迎指出,一起讨论。