[做题笔记] 经典省选题补做

我的博客大概要封笔了,最后一周也不会做什么题了,再见了朋友们。

[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]);
		}
	}
}
posted @ 2022-03-27 16:53  C202044zxy  阅读(299)  评论(7编辑  收藏  举报