[做題筆記] 經典省選題補做


我的博客大概要封筆了,最后一周也不會做什么題了,再見了朋友們。

[HNOI2014] 道路堵塞

題目描述

點此看題

解法

我們不妨考慮增量法,先把在最短路徑上的邊排除掉,跑完最短路之后再慢慢添加邊。

如果我們要求刪除邊 \(i\) 的答案,那么我們需要添加邊 \([1,i)\),並且考慮 \((i,k]\) 邊的影響(這些邊我們是不加的),考慮把 \((i,k]\) 構成的路徑染色,那么如果我們到達的某個點被染色,那么可以直接走最短路到終點

為了保證復雜度我們把給定的最短路徑染色,如果現在 \(\tt spfa\) 更新到了最短路上的第 \(i\) 個點,那么我們這條路徑打上時間戳 \(i\),如果刪除的邊 \(\in(i,k]\),那么這條拼湊出來的路徑是對答案有貢獻的,用一個堆維護即可。

時間復雜度基於 \(\tt spfa\),所以在不刻意卡的情況是可以通過的。

還有一種時間復雜度穩定的最短路樹做法,找機會填坑。

總結

圖論中的一些動態算法十分重要,巧用動態算法可以快速完成版本之間的轉化,以解決一維偏序關系。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std; 
const int M = 200005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,tot,f[M],id[M],d[M],in[M];
queue<int> q;int rd[M],p[M],g[M],ban[M];
struct edge{int v,c,next;}e[M];
struct node
{
	int u,c;
	bool operator < (const node &b) const
		{return c>b.c;}
};priority_queue<node> s;
void spfa(int now)
{
	q.push(now);in[now]=1;
	while(!q.empty())
	{
		int u=q.front();q.pop();in[u]=0;
		for(int i=f[u];i;i=e[i].next)
		{
			int v=e[i].v,c=e[i].c;
			if(ban[i]) continue;
			if(d[v]>d[u]+c)
			{
				d[v]=d[u]+c;
				if(id[v]) s.push({id[v],d[v]+g[id[v]]}); 
				else if(!in[v]) in[v]=1,q.push(v);
			}
		}
	}
}
signed main()
{
	n=read();m=read();k=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),c=read();
		e[++tot]=edge{v,c,f[u]},f[u]=tot;
	}
	for(int i=1;i<=k;i++)
	{
		rd[i]=read();ban[rd[i]]=1;
		p[i+1]=e[rd[i]].v;id[p[i+1]]=i+1;
	}
	p[1]=1;id[1]=1;
	for(int i=k;i>=1;i--) g[i]=g[i+1]+e[rd[i]].c;
	memset(d,0x3f,sizeof d);
	d[1]=0;spfa(1);
	for(int i=1;i<=k;i++)
	{
		while(!s.empty() && s.top().u<=i) s.pop();
		if(s.empty()) puts("-1");
		else printf("%d\n",s.top().c);
		d[p[i+1]]=d[p[i]]+e[rd[i]].c;
		spfa(p[i+1]);
	}
}

[CQOI2017] 小Q的表格

題目描述

點此看題

解法

用了一個多小時手切了這道題,雖然是黑題但是聽說特別簡單,沒啥成就感。

首先思考什么節點會被影響到,觀察 \(f\) 下標的形式是 \((a,b)\rightarrow (a,a+b)\) 的形式,但凡帶點腦子的都可以想到輾轉相除法,所以可以給出關鍵 \(\tt observation\):修改 \((x,y)\) 影響且只會影響到 \(\gcd(x,y)=\gcd(a,b)\) 的點 \((a,b)\)

並且修改的影響是相對於初始值整體乘上一個倍數 \(z\)(注意 \(z\) 不一定是整數),考慮按 \(\tt gcd\) 種類分類算貢獻,那么問題轉化成求出 \(k\) 正方形范圍內,\(\gcd(a,b)=x\)\(a\cdot b\) 之和,直接開始推式子:

\[\begin{aligned}&\sum_{i=1}^k\sum_{j=1}^k [(i,j)=x]\cdot ij\\=&x^2\cdot \sum_{i=1}^{k/x}\sum_{j=1}^{k/x} [(i,j)=1]\cdot ij\\=&x^2\cdot \sum_{i=1}^{k/x}\sum_{j=1}^{k/x} \sum_{d|(i,j)}\mu(d)\cdot ij\\=&x^2\cdot \sum_{d=1}^{k/x}\mu(d)\cdot d^2\cdot(\sum_{i=1}^{k/xd} i)^2\end{aligned} \]

那么單次求是 \(O(\sqrt n)\) 的整除分塊,可以把這東西記憶化,時間復雜度 \(O(m^2+n\sqrt n)\),可以跑過省選的原數據,但是在 \(\tt luogu\) 上被卡了。正確的思路是把所有 \(\tt gcd\) 混在一起推式子,也不是特別難。

#include <cstdio>
#include <cassert>
const int N = 10005;
const int M = 4000005;
const int MOD = 1e9+7;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int m,n,cnt,p[M],vis[M],mu[M],s2[M];
int t,a[N],b[N],f[M],s[M];
int gcd(int a,int b) {return !b?a:gcd(b,a%b);}
int sum(int x) {return x*(x+1)/2%MOD;}
int sqr(int x) {return x*x%MOD;} 
void init(int n)
{
	mu[1]=1;
	for(int i=2;i<=n;i++)
	{
		if(!vis[i]) p[++cnt]=i,mu[i]=-1;
		for(int j=1;j<=cnt && i*p[j]<=n;j++)
		{
			vis[i*p[j]]=1;
			if(i%p[j]==0) break;
			mu[i*p[j]]=-mu[i];
		}
	}
	for(int i=1;i<=n;i++)
		s2[i]=(s2[i-1]+mu[i]*i*i)%MOD;
	for(int i=1;i<=n;i++)
		s[i]=(s[i-1]+i*(sum(i)+sum(i-1)))%MOD;
}
int get(int n)
{
	if(f[n]) return f[n];
	int res=0;
	for(int l=1,r=1;l<=n;l=r+1)
	{
		r=n/(n/l);
		res=(res+(s2[r]-s2[l-1])*sqr(sum(n/l)))%MOD;
	}
	return f[n]=res;
}
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
void work()
{
	int A=read(),B=read(),x=read(),k=read();
	int y=gcd(A,B),fl=0,ans=s[k];
	x=x%MOD*qkpow(A*B%MOD,MOD-2)%MOD;
	for(int i=1;i<=t;i++) if(a[i]==y)
		{fl=1;b[i]=x;break;}
	if(!fl) a[++t]=y,b[t]=x;
	for(int i=1;i<=t;i++)
		ans=(ans+(b[i]-1)*get(k/a[i])
		%MOD*sqr(a[i]))%MOD;
	printf("%lld\n",(ans+MOD)%MOD);
}
signed main()
{
	m=read();n=read();init(n);
	for(int i=1;i<=m;i++) work();
}

[CQOI2017] 老C的方塊

題目描述

點此看題

解法

一開始的結論假了,導致做法也假了,實際上還是題目沒有讀清楚

根據套路,我們嘗試把限制表達成路徑,然后跑最小割來解決。

首先要確定路徑的形式,我們先猜想路徑存在一種簡單的統一形式,發現可以染色的方式給出構造:

img

那么路徑一定是 \(1\rightarrow 2\rightarrow 3\rightarrow 4\) 的形式,那么可以建立一個分層圖,如果兩點相鄰並且層也相鄰,就把他們連起來。因為割的是點權,所以還需要拆點以轉化為邊權。

#include <cstdio>
#include <iostream>
#include <unordered_map>
#include <queue>
using namespace std;
const int M = 100005;
const int N = 200005;
const int inf = 0x3f3f3f3f;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,a[M],b[M],c[M],w[M],S,T,tot,f[N],cur[N],d[N];
int dx[4]={1,-1},dy[4]={0,0,1,-1};
unordered_map<int,int> mp[M];
struct edge{int v,c,next;}e[10*M];
void add(int u,int v,int c)
{
	e[++tot]=edge{v,c,f[u]},f[u]=tot;
	e[++tot]=edge{u,0,f[v]},f[v]=tot;
}
int bfs()
{
	queue<int> q;q.push(S);d[S]=1;
	for(int i=1;i<=T;i++) d[i]=0;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		if(u==T) return 1;
		for(int i=f[u];i;i=e[i].next)
		{
			int v=e[i].v;
			if(!d[v] && e[i].c>0)
			{
				d[v]=d[u]+1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int u,int ept)
{
	if(u==T) return ept;
	int tmp=0,flow=0;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(d[v]==d[u]+1 && e[i].c>0)
		{
			tmp=dfs(v,min(ept,e[i].c));
			if(!tmp) continue;
			ept-=tmp;flow+=tmp;
			e[i].c-=tmp;e[i^1].c+=tmp;
			if(!ept) break;
		}
	}
	return flow;
}
signed main()
{
	read();read();n=read();S=0;T=2*n+2;tot=1;
	for(int i=1;i<=n;i++)
	{
		b[i]=read();a[i]=read();w[i]=read();
		mp[a[i]][b[i]]=i;
		if(a[i]&1) c[i]=4-(b[i]%4);
		else c[i]=(b[i]%4+2)%4+1;
		add(i<<1,i<<1|1,w[i]);
		if(c[i]==1) add(S,i<<1,inf);
		if(c[i]==4) add(i<<1|1,T,inf);
	}
	for(int i=1;i<=n;i++) for(int j=0;j<4;j++)
	{
		int x=a[i]+dx[j],y=b[i]+dy[j];
		if(x<=0 || y<=0 || !mp[x].count(y)) continue;
		int to=mp[x][y];
		if(c[i]+1==c[to]) add(i<<1|1,to<<1,inf);
	}
	int ans=0;
	while(bfs())
	{
		for(int i=S;i<=T;i++) cur[i]=f[i];
		ans+=dfs(S,inf);
	}
	printf("%d\n",ans);
}

[JSOI2009] 等差數列

題目描述

點此看題

解法

沒想到啊,等差數列轉差分具有十分簡潔的形式,設 \(c_i=a_{i+1}-a_i\),我們維護一個長度為 \(n-1\) 的差分數組 \(c\),那么每次修改相當於兩個單點修改,加上一個區間修改。

考慮詢問,發現等差數列一定是 \(c\) 連續相同的一段,若 \(\forall i\in[l,r],c_i=c_l\),則這個等差數列可以覆蓋點 \([l,r+1]\),我們需要找到最少的等差數列使得所有點都被覆蓋,這里有一個誤區就是答案並不等於 \(c\) 相同連續段的個數。

考慮把它放在線段樹上維護,設 \(s[0/1/2/3]\) 分別表示 左右端點都不被覆蓋 \(/\) 左端點被覆蓋 \(/\) 右端點被覆蓋 \(/\) 左右端點都被覆蓋 的最小等差數列數。合並的時候需要保證中間的點被左邊或者右邊覆蓋才行,如果被兩邊都覆蓋了,那么可以考慮判斷合並兩個等差數列。

時間復雜度 \(O(n\log n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a[M];
struct node {int l,r,fl,s[4];}t[M<<2];
void upd(int &x,int y) {x=min(x,y);}
node operator + (node a,node b)
{
	node w;w.l=a.l;w.r=b.r;w.fl=0;
	int z=a.r==b.l; 
	w.s[0]=a.s[2]+b.s[1]-z;
	upd(w.s[0],a.s[2]+b.s[0]);
	upd(w.s[0],a.s[0]+b.s[1]);
	//
	w.s[1]=a.s[3]+b.s[1]-z;
	upd(w.s[1],a.s[3]+b.s[0]);
	upd(w.s[1],a.s[1]+b.s[1]);
	//
	w.s[2]=a.s[2]+b.s[3]-z;
	upd(w.s[2],a.s[2]+b.s[2]);
	upd(w.s[2],a.s[0]+b.s[3]);
	//
	w.s[3]=a.s[3]+b.s[3]-z;
	upd(w.s[3],a.s[3]+b.s[2]);
	upd(w.s[3],a.s[1]+b.s[3]);
	return w;
}
void fuck(int i,int c)
{
	if(!i) return ;
	t[i].l+=c;t[i].r+=c;t[i].fl+=c;
}
void down(int i)
{
	if(!t[i].fl) return ;
	fuck(i<<1,t[i].fl);
	fuck(i<<1|1,t[i].fl);t[i].fl=0;
}
void build(int i,int l,int r)
{
	if(l==r)
	{
		t[i].l=t[i].r=a[l];
		t[i].s[1]=t[i].s[2]=t[i].s[3]=1;
		return ;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	t[i]=t[i<<1]+t[i<<1|1];
}
void ins(int i,int l,int r,int L,int R,int c)
{
	if(L>r || l>R) return ;
	if(L<=l && r<=R) {fuck(i,c);return ;}
	int mid=(l+r)>>1;down(i);
	ins(i<<1,l,mid,L,R,c);
	ins(i<<1|1,mid+1,r,L,R,c);
	t[i]=t[i<<1]+t[i<<1|1];
}
node ask(int i,int l,int r,int L,int R)
{
	if(L<=l && r<=R) return t[i];
	int mid=(l+r)>>1;down(i);
	if(mid<L) return ask(i<<1|1,mid+1,r,L,R);
	if(R<=mid) return ask(i<<1,l,mid,L,R);
	return ask(i<<1,l,mid,L,R)
	+ask(i<<1|1,mid+1,r,L,R); 
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<n;i++) a[i]=a[i+1]-a[i];
	build(1,1,n);
	m=read();char s[4];
	while(m--)
	{
		scanf("%s",s);int l=read(),r=read();
		if(s[0]=='A')
		{
			int a=read(),b=read();
			if(l>1) ins(1,1,n,l-1,l-1,a);
			if(r<n) ins(1,1,n,r,r,-a-b*(r-l));
			if(l^r) ins(1,1,n,l,r-1,b);
		}
		if(s[0]=='B')
		{
			if(l==r) {puts("1");continue;}
			printf("%lld\n",ask(1,1,n,l,r-1).s[3]);
		}
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM