1. 程式人生 > 實用技巧 >我們仍未知道那天所看見的題的解法

我們仍未知道那天所看見的題的解法

目錄

  • P1892 [BOI2003]團伙
  • P6747 『MdOI R3』Teleport
  • CF1163F Indecisive Taxi Fee

P1892 [BOI2003]團伙

分析過程

初看題面,直覺就告訴我這是一個並查集題

然後樣例一遍秒了,接著開始口胡資料來做

最難考慮的是類似遞迴的情況,即\(x_1\)的敵人是\(x_2\)\(x_2\)的敵人是\(x_3\)……以此類推

這個時候考慮使用反集在n個人中的,a與b是敵人,b與c是敵人,則會出現以下合併場面

用這張圖可以考慮反集的正確性

程式碼

#include<iostream>
#include<cstdio>
using namespace std;
int fa[1000001],a,b,n,m,ans;
char c;
inline int find(int n){
	if(fa[n]!=n) fa[n]=find(fa[n]);
	return fa[n];
}
inline void unionn(int x,int y){
	x=find(x),y=find(y);
	fa[y]=x;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=(n<<1);++i){
		fa[i]=i;
	}
	for(int i=1;i<=m;++i){
		cin>>c>>a>>b;
		if(c=='F') unionn(a,b);
		else{
			unionn(a,n+b);
			unionn(b,n+a);
		}
	}
	for(int i=1;i<=n;i++){
		if(fa[i]==i) ans+=1;
	}
	printf("%d\n",ans);
}

總結

在並查集中時常考慮反集的使用

P6747 『MdOI R3』Teleport

分析過程

初看題面,這就是在求:

一開始可以按照異或的性質來做,實質上就是縮小列舉的區間
兩個數異或必定大於的數為左端點,兩個數必定小於的數為右端點

但是閉著眼睛都能想到能卡這個程式的資料

沒有在二進位制的層面考慮問題

考慮按位貪心

由於二進位制運算都具有獨立性,也就是說當某一位進行運算的時候其他的任何一位都不會受到影響
所以貪心是完全可行的

考慮二進位制的運演算法則,在每一位貪心的時候儘量選擇1,否則就選0

由於存在不合法的情況,我們可以預處理0~n位的最小代價

程式碼

#include<iostream>
#include<cstdio>
#define int long long
#define N 1001000
using namespace std;
int wei[61],n,q,will,sum,ans,in,minn,amin[61];
inline int min(int a,int b){
	return a>b?b:a;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>in;
		for(int o=0;o<31;o++) wei[o]+=(bool)(in&(1ll<<o));
	}
	for(int i=0;i<=45;i++){
		amin[i]=amin[i-1]+min(wei[i]*(1ll<<i),(n-wei[i])*(1ll<<i));
	}
	cin>>q;
	while(q--){
		cin>>will;
		sum=0;ans=0;
		if(will<amin[45]){
			cout<<"-1\n";
			continue;
		}
		for(int i=45;i>=0;i--){
			if(sum+(1ll<<i)*(n-wei[i])+(i==0?0:amin[i-1])<=will){
				sum+=(1ll<<i)*(n-wei[i]);
				ans+=(1ll<<i);
			}
			else sum+=(1ll<<i)*wei[i];
		}
		cout<<ans<<"\n";
	}
}

總結

對於二進位制運算,一定要在二進位制的層面想一想能否用到基礎演算法

CF1163F Indecisive Taxi Fee

【變數含義】

posdis[i]:i點到起點的最短路

invdis[i]:i點到終點的最短路

dis[i]:1到i的最短路

fr[i]:i這條邊的起點

to[i]:i這條邊的終點

w[i]:i這條邊的權值

【分析過程】

對於原圖,當某條邊的權值被修改之後,圖內的最短路的長度

首先思考暴力做法
很簡單,修改之後跑最短路即可,時間複雜度爆炸


考慮一道圖論題:求圖上經過某一條邊的最短路徑
很簡單吧?正向建邊+反向建邊,分別從起點與終點跑一次最短路,求出每個點到起點的最短路徑與到終點的最短路徑

答案就是:\(min(posdis[fr[i]]+invdis[to[i]]+w[i],posdis[to[i]]+inv[fr[i]]+w[i])\)


這麼一想,似乎這個題可能用到這個思想

藉助題解思考之後得到了接下來的思路

考慮特殊情況,當修改的邊不在最短路上,並且這條邊的權值被修改小了,我們就可以用這個方法比個大小,答案就出來了

這是一種情況
一提到“情況”這個詞,不難發現這道題可以分類討論


【情況一】當修改的邊在最短路的路徑上,並且權值被修改小了
【做法】直接輸出起點到終點的最短路減去差值
【正確性】

  • 本來就是最短路了,路徑還被修改小了,那當然是最小的

【情況二】當修改的邊不在最短路的路徑上,並且權值被修改小了
【做法】比較這兩條路徑即可:\(min(dis[n],min(posdis[fr[i]]+invdis[to[i]]+w[i],posdis[to[i]]+inv[fr[i]]+w[i]))\)
【正確性】

  • 沒被修改之前所有的路徑肯定都是大於等於最短路的值的,修改之後只有存在被修改的這條邊的路徑的最短路徑的值發生了變化
  • 這個路徑有很多,但是我們關心的只是最小值,即經過這條邊的最短路,由上可得做法正確

【情況三】當修改的邊不在最短路的路徑上,並且權值被修改大了
【做法】輸出\(dis[n]\)即可
【正確性】

  • 顯然

【情況四】當修改的邊在最短路的路徑上,並且權值被修改大了
這種是最麻煩的,本來以為和k短路做法差不多,然後貪心就行了
結果發現還是我太弱了,因為次短路,次次短路都有可能經過這條邊
當這條邊被修改的時候,會影響很多路徑

考慮從最終結果逆推,最後輸出的答案肯定是\(dis[n]\)加上差值,然後與不經過這條邊的最短路比個大小
區間操作啊...肯定是使用線段樹維護啦

我們指定一條最短路徑,然後用不在這條路徑上的邊去更新\([l,r]\)

例如邊\(2\to4\),就可以更新藍色區間:
紅色邊為最短路徑

\(l\)\(r\)是?

對於一條繞過一條在最短路徑上的邊的路徑

它肯定是在在原來的最短路徑上的某個點\(p_1\)分叉,經過這條邊,然後又在在原來的最短路徑上的某個點\(p_2\)回來,例如:

(紅色邊路徑為最短路)

當不經過邊\(2\to6\)
可能從6分叉,沿橙色路徑到5匯合,也可能直接沿藍色路徑從終點匯合

規定:

  • 最短路徑上的邊的方向為到終點的方向
  • 這裡所說的入邊為在最短路徑上的邊

\(l\)就是\(p_1\)的入邊,\(r\)就是\(p_2\)的入邊

當從\(5\)匯合時,邊\(5\to8\)就能更新路徑\(1\to6\to2\to3\to4\to5\)
也就是\(p_1\to p_2\)

沒有寫具體數值就是為了一圖多用,能夠模擬更多情況

誒,發現了沒有,這就是問你經過某條指定邊的最短路的時候順便做的事情

最後查詢即可

【程式碼實現】

#include<iostream>
#include<cstdio>
#include<queue>
#define int long long
#define N 1000001
#define INF 999999999999999
using namespace std;
int n,m,q,fr[N],to[N],w[N],bf,head[N],num,posdis[N],invdis[N],now,pathnum,ce,cv,ans,redge[N],pathnumrc[N],*toa,l[N],r[N];
bool on[N],vis[N];
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >qu;
struct seg {int v;} t[N];
struct Edge {int fp,na,np,w;} e[N<<2];
inline int min(int a,int b){return a>b?b:a;}
inline void add(int f,int t,int w){
	e[++num].na=head[f];
	e[num].fp=f;e[num].np=t;e[num].w=w;
	head[f]=num;
}
inline void dij(int s,int *pa,short sta){
	for(int i=1;i<=n;i++) *(pa+i)=INF,vis[i]=0;
	*(pa+s)=0;
	qu.push(make_pair(0,s));
	while(!qu.empty()){
		bf=qu.top().second;qu.pop();
		if(vis[bf]) continue;
		vis[bf]=1;
		for(int i=head[bf];i;i=e[i].na){
			if(*(pa+bf)+e[i].w<*(pa+e[i].np)){
				redge[e[i].np]=i;
				*(pa+e[i].np)=*(pa+bf)+e[i].w;
				qu.push(make_pair(*(pa+e[i].np),e[i].np));
				if(!on[e[i].np]){
					if(sta==1) l[e[i].np]=l[bf];
					else if(sta==2) r[e[i].np]=r[bf];
				}
			}
		}
	}
}
inline void build(int node,int l,int r){
	t[node].v=INF;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(node<<1,l,mid);
	build(node<<1|1,mid+1,r);
}
inline void upd(int node,int fl,int fr,int ul,int ur,int v){
	if(ul>ur) return;
	if(ul<=fl&&fr<=ur){
		t[node].v=min(t[node].v,v);
		return;
	}
	int mid=(fl+fr)>>1;
	if(ul<=mid) upd(node<<1,fl,mid,ul,ur,v);
	if(mid<ur) upd(node<<1|1,mid+1,fr,ul,ur,v);
}
inline int query(int node,int l,int r,int rc){
	if(l==r) return t[node].v;
	int mid=(l+r)>>1,rans=t[node].v;
	if(rc<=mid) rans=min(rans,query(node<<1,l,mid,rc));
	else rans=min(rans,query(node<<1|1,mid+1,r,rc));
	return rans;
}
signed main(){
	scanf("%lld%lld%lld",&n,&m,&q);
	for(int i=1;i<=m;i++){
		scanf("%lld%lld%lld",&fr[i],&to[i],&w[i]);
		add(fr[i],to[i],w[i]);
		add(to[i],fr[i],w[i]);
		pathnumrc[i]=pathnumrc[m+i]=-1;
	}
	toa=invdis;dij(n,toa,0);
	on[1]=1;pathnum=l[1]=r[1]=0;now=1;
	while(now!=n){
		pathnumrc[redge[now]]=pathnum+1;
		if(redge[now]%2) pathnumrc[redge[now]+1]=pathnum+1;
		else pathnumrc[redge[now]-1]=pathnum+1;
		pathnum+=1;
		now=e[redge[now]].fp^e[redge[now]].np^now;
		on[now]=1;l[now]=r[now]=pathnum;
	}
	toa=posdis;dij(1,toa,1);
	toa=invdis;dij(n,toa,2);
	build(1,1,pathnum);
	for(int i=1;i<=num;i++)
		if(pathnumrc[i]==-1)
			upd(1,1,pathnum,l[e[i].fp]+1,r[e[i].np],posdis[e[i].fp]+invdis[e[i].np]+e[i].w);
	for(int i=1;i<=q;i++){
		scanf("%lld%lld",&ce,&cv);
		ans=posdis[n];
		if(pathnumrc[ce<<1]==-1){
			if(cv<w[ce])
				ans=min(ans,min(posdis[fr[ce]]+invdis[to[ce]],posdis[to[ce]]+invdis[fr[ce]])+cv);
		}
		else{
			ans=ans-w[ce]+cv;
			if(cv>w[ce]) ans=min(ans,query(1,1,pathnum,pathnumrc[ce<<1]));
		}
		printf("%lld\n",ans);
	}
}

【答疑解惑】

針對閱讀程式碼的時候可能產生的問題進行回答:

  • 注意到我們將最短路徑上的邊進行了連續的標號處理,所以區間\([2,5]\)指的是最短路徑上的第二條邊到第五條邊
  • 因為有的時候邊的起點與終點是反的,根據兩個相同的數異或結果為0,0與任何數異或都為原數這些性質,我們自然能夠正確的找到下一個\(now\)的位置
  • 因為是雙向邊自然要有選擇性的進行處理,例如乘二,判斷奇偶等等
  • 第一遍為啥是反著搜?——你正著試試

【後記】

真正理解了這道題,你是不是覺得很簡單呢?
總結經驗與教訓,會對你有很大的幫助

歡迎Hack!!