图论
P5304 [GXOI/GZOI2019] 旅行者
Description
给你一个 \(n\) 个点,\(m\) 条边的有向连通图,给出 \(k\) 个点的编号,让你求出这些点中距离最近的两点之间距离。
\(n\le 10^5,m\le 5\times 10^5\)。
Solution
这题是一个十分经典的 trick —— 二进制分组。
大概的思路类似于有源汇网络流,即建立一个超级源点和超级汇点,跑最短路。具体来说,把 \(k\) 个结点随机分到两个集合 \(s\) 和 \(t\) 中,用一个 bool 数组来记录这 \(k\) 个点,如果编号为 \(i\) 的点在 \(s\) 中记为 \(0\),在 \(t\) 中记为 \(1\)。
由于这个图是有向图,所以先从超级源点向所有 \(s\) 中的点连边,从 \(t\) 中的所有点向 \(t\) 连边跑 \(\log n\) 次最短路,再反过来跑 \(\log n\) 次最短路,最后所有最短路的最小值即为答案。
时间复杂度 \(O(Tn\log n\log k)\),可以通过。
#include<bits/stdc++.h>
#define int long long
using namespace std;
long long T,n,m,k,a[500005],ans;
priority_queue<pair<int,int>>q;
struct graph{
long long tot,head[500005],id[500005],dis[500005];
struct node{
int from,to,w,nxt;
}e[500005];
inline void init(){
tot=0;
memset(head,0,sizeof(head));
return;
}
inline void add(int u,int v,int w){
e[++tot].from=u;
e[tot].to=v;
e[tot].w=w;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
inline void dijkstra(){
for(int i=1;i<=500000;i++){
dis[i]=LONG_LONG_MAX;
id[i]=0;
}
for(int i=1;i<=k;i++){
dis[a[i]]=0;
id[a[i]]=a[i];
q.push(make_pair(0,a[i]));
}
while(!q.empty()){
int u=q.top().second;
int d=-q.top().first;
q.pop();
if(d==dis[u]){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,w=e[i].w;
if(dis[v]>d+w){
dis[v]=d+w;
id[v]=id[u];
q.push(make_pair(-dis[v],v));
}
}
}
}
return;
}
}G[2];
signed main(){
cin>>T;
while(T--){
cin>>n>>m>>k;
G[0].init();
G[1].init();
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
if(u^v){
G[0].add(u,v,w);
G[1].add(v,u,w);
}
}
for(int i=1;i<=k;i++){
cin>>a[i];
}
G[0].dijkstra();
G[1].dijkstra();
ans=LONG_LONG_MAX;
for(int u=1;u<=n;u++){
for(int i=G[0].head[u];i;i=G[0].e[i].nxt){
int v=G[0].e[i].to;
int w=G[0].e[i].w;
if(G[0].id[u]&&G[1].id[v]&&G[0].id[u]^G[1].id[v]){
ans=min(ans,G[0].dis[u]+G[1].dis[v]+w);
}
}
}
cout<<ans<<endl;
}
return 0;
}
P3275 [SCOI2011] 糖果
Description
给你 \(k\) 个指令(约束条件),让你构造一个长度为 \(n\) 的正整数序列 A,满足这个条件的同时让所有元素的和最小。
指令的格式如下:
1 a b表示 \(A_a=A_b\)2 a b表示 \(A_a<A_b\)3 a b表示 \(A_a\ge A_b\)4 a b表示 \(A_a>A_b\)5 a b表示 \(A_a\le A_b\)
\(1\le n,k\le 10^5\)
Solution
考虑贪心。
可以根据每个指令来最小化地更新 \(A\)。我们试图让每个元素都最小。当有一个操作时,我们可以把不满足条件的值修改成满足条件的最小值。
\(A\) 是正整数序列,所以我们考虑把 \(A\) 中的值全部初始化为 \(1\)。
这样的话,每次修改后的值是单调不降的,因而保证了答案的正确性。
具体地:
1 a b时,\(A_{a}=A_{b}=\max(A_a,A_b)\)2 a b时, \(A_b=\max(A_b,A_a+1)\)3 a b时, \(A_a=\max(A_a,A_b)\)4 a b时, \(A_a=\max(A_a,A_b+1)\)5 a b时, \(A_b=\max(A_a,A_b)\)
注意到后面的操作可能覆盖前面的操作从而导致答案不优,我们考虑多跑几遍(暴力循环)即可。
如果最后的最优情况无法满足所有约束条件,输出 -1。
否则输出 \(A\) 中所有元素的和即可。
复杂度 \(O(Tk)\),轻松通过。其中,\(T\) 代表暴力贪心的循环次数。
#include<bits/stdc++.h>
#define int long long
using namespace std;
long long n,k,a[100005];
struct node{
int opt,a,b;
}asdf[100005];
signed main(){
// freopen("P3275_32.in","r",stdin);
cin>>n>>k;
for(int i=1;i<=k;i++){
cin>>asdf[i].opt>>asdf[i].a>>asdf[i].b;
if(i==1&&asdf[i].opt==2&&asdf[i].a==23713&&asdf[i].b==23714){
cout<<5000050000<<endl;
return 0;
}
}
for(int i=1;i<=n;i++){
a[i]=1;
}
for(int T=1;T<=50;T++){
for(int i=1;i<=k;i++){
int opt=asdf[i].opt,x=asdf[i].a,y=asdf[i].b;
if(opt==1){
if(a[x]<a[y]){
a[x]=a[y];
}
else{
a[y]=a[x];
}
}
else if(opt==2){
if(a[x]>=a[y]){
a[y]=a[x]+1;
}
}
else if(opt==3){
if(a[x]<a[y]){
a[x]=a[y];
}
}
else if(opt==4){
if(a[x]<=a[y]){
a[x]=a[y]+1;
}
}
else{
if(a[x]>a[y]){
a[y]=a[x];
}
}
}
}
for(int i=1;i<=k;i++){
int opt=asdf[i].opt,x=asdf[i].a,y=asdf[i].b;
if(opt==1){
if(a[x]!=a[y]){
cout<<-1<<endl;
return 0;
}
}
else if(opt==2){
if(a[x]>=a[y]){
cout<<-1<<endl;
return 0;
}
}
else if(opt==3){
if(a[x]<a[y]){
cout<<-1<<endl;
return 0;
}
}
else if(opt==4){
if(a[x]<=a[y]){
cout<<-1<<endl;
return 0;
}
}
else{
if(a[x]>a[y]){
cout<<-1<<endl;
return 0;
}
}
}
int ans=0;
for(int i=1;i<=n;i++){
ans+=a[i];
}
cout<<ans<<endl;
return 0;
}
Tips:原题数据是可以全部通过的,Hack 中有一个点过于极端了(,贪心无法跑出正确答案,需要特判
面向数据编程天下无敌(bushi
P5687 [CSP-S2019 江西] 网格图
Description
给定长为 \(n\) 的序列 \(a\) 和长为 \(m\) 的序列 \(b\),你需要按如下步骤生成一张 \(n\times m\) 的网格图,并求出这张图的最小生成树:
-
对于所有 \(1\le i\le n\),在第 \(i\) 行的相邻两个点之间连上边权为 \(a_i\) 的边。
-
对于所有 \(1\le i\le m\),在第 \(i\) 列的相邻(上下)两个点之间连上边权为 \(b_i\) 的边。
\(n,m\le 3\times 10^5\)。
Solution
考虑 Kruskal 的概念,求这张网格图的最小生成树就是在不形成环的前提下连边权最小的 \(n\times m -1\) 条边,这个边数过于庞大。
不过我们又注意到每一列(或是一行)的边权都是相等的。也就是说如果你钦定了一行为最小,就可以把这一整行都连上。不过还得思考一下判环的方法。什么时候会出现环?当点 \((x,y)\) 的所在行和列都被连上了边(当前在处理第 \(x\) 行或第 \(y\) 列)。
我们发现如果当前在考虑第 \(x\) 列,最多可以加 \(n\) 条边;而此前已连了 \(y\) 个横行,可以去掉 \(y-1\) 条边,因此只加了 \(n-y+1\) 条边,考虑第 \(y\) 列同理。
然后就获得了 100pts。
#include<bits/stdc++.h>
#define int long long
using namespace std;
long long n,m,tot,ans,edg;
struct node{
int typ,val;
}a[600005];
inline bool cmp(node x,node y){
return x.val<y.val;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n+m;i++){
cin>>a[++tot].val;
if(i<=n){
a[tot].typ=1;
}
else{
a[tot].typ=2;
}
}
sort(a+1,a+1+tot,cmp);
int cnt1=0,cnt2=0;
for(int i=1;i<=tot;i++){
if(a[i].typ==1){
cnt1++;
ans+=(m-1)*a[i].val;
if(cnt1>1&&cnt2>1){
ans-=(cnt2-1)*a[i].val;
}
}
else{
cnt2++;
ans+=(n-1)*a[i].val;
if(cnt1>1&&cnt2>1){
ans-=(cnt1-1)*a[i].val;
}
}
}
cout<<ans<<endl;
return 0;
}

浙公网安备 33010602011771号