[做题笔记] 经典省选题补做
我的博客大概要封笔了,最后一周也不会做什么题了,再见了朋友们。
[HNOI2014] 道路堵塞
题目描述
点此看题
解法
我们不妨考虑增量法,先把在最短路径上的边排除掉,跑完最短路之后再慢慢添加边。
如果我们要求删除边 \(i\) 的答案,那么我们需要添加边 \([1,i)\),并且考虑 \((i,k]\) 边的影响(这些边我们是不加的),考虑把 \((i,k]\) 构成的路径染色,那么如果我们到达的某个点被染色,那么可以直接走最短路到终点。
为了保证复杂度我们把给定的最短路径染色,如果现在 \(\tt spfa\) 更新到了最短路上的第 \(i\) 个点,那么我们这条路径打上时间戳 \(i\),如果删除的边 \(\in(i,k]\),那么这条拼凑出来的路径是对答案有贡献的,用一个堆维护即可。
时间复杂度基于 \(\tt spfa\),所以在不刻意卡的情况是可以通过的。
还有一种时间复杂度稳定的最短路树做法,找机会填坑。
总结
图论中的一些动态算法十分重要,巧用动态算法可以快速完成版本之间的转化,以解决一维偏序关系。
#include
#include
#include
#include
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 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 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
#include
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的方块
题目描述
点此看题
解法
一开始的结论假了,导致做法也假了,实际上还是题目没有读清楚。
根据,我们尝试把限制表达成路径,然后跑最小割来解决。
首先要确定路径的形式,我们先猜想路径存在一种简单的统一形式,发现可以染色的方式给出构造:
那么路径一定是 \(1\rightarrow 2\rightarrow 3\rightarrow 4\) 的形式,那么可以建立一个分层图,如果两点相邻并且层也相邻,就把他们连起来。因为割的是点权,所以还需要拆点以转化为边权。
#include
#include
#include
#include
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 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 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
#include
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(mid1) ins(1,1,n,l-1,l-1,a);
if(r