Logo KSCD_ 的博客

博客

标签
暂无

文以载道(互测赛) CDGH 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-04-14 20:58:03

C.十年灯

题意

给出 $n$ 个点 $m$ 条边的无向图,在出发前可以进行下面两种操作其中一种任意次。

  • 选择一个点的两条出边交换权值。
  • 选择一个点连着的任意两条边交换权值。

求在最优操作后 $1$ 到 $n$ 的最短路。

思路

本题的关键在于两种操作,我们分别来分析。

对于第一种操作,我们考虑到最短路只会经过每个点最多一次,因此每个点的所有出边只会走最多一条,此时把最小的出边权值交换到这条边上即可。所以记录每个点最小的出边权值,在跑最短路更新时均用该权值更新即可。

对于第二种操作,我们发现实际上由于操作次数不限,整张图上所有边的权值均可以随意交换,因此我们只需要 Bfs 找到 $1$ 到 $n$ 所需的最少边数 $k$,再将最小的 $k$ 个边权交换过来即可。

代码

#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e5+10;
const int M=4e5+10;
const int inf=1e18;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') { s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int id,n,m,cnt,md[N],mn[N],d[N],s[M];
bool v[N];
struct edg
{
	int v,w;
};
vector <edg> edge[N];
void sol1()
{
	priority_queue <pii > q;
	for(int i=1;i<=n;i++)
	{
		mn[i]=d[i]=inf;
		for(edg e:edge[i]) mn[i]=min(mn[i],e.w);
	} 
	d[1]=0,q.push({0,1});
	while(!q.empty())
	{
		int u=q.top().second; q.pop();
		if(v[u]) continue;
		v[u]=1;
		for(edg e:edge[u])
		{
			int ne=e.v;
			if(d[ne]>d[u]+mn[u])
			{
				d[ne]=d[u]+mn[u];\/\/使用最小出边权更新 
				q.push({-d[ne],ne});
			}
		}
	}\/\/Dijkstra 
	cout<<d[n];
}
void sol2()
{
	queue <int> q;
	for(int i=1;i<=n;i++)
	{
		d[i]=inf;
		for(edg e:edge[i]) s[++cnt]=e.w;
	}
	sort(s+1,s+1+cnt);
	d[1]=0,q.push(1);
	while(!q.empty())
	{
		int u=q.front(); q.pop();
		if(v[u]) continue;
		v[u]=1;
		for(edg e:edge[u])
		{
			int ne=e.v;
			if(!v[ne])
			{
				d[ne]=min(d[ne],d[u]+1);
				q.push(ne);
			}
		}
	}\/\/Bfs求1到每个点需要的最小边数 
	int res=0;
	for(int i=1;i<=d[n];i++) res+=s[i];\/\/选最小的若干个边权 
	cout<<res;
}
signed main()
{
	id=read(),n=read(),m=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read();
		edge[u].push_back({v,w});
	}
	if(id==1) sol1();
	else sol2(); 
	return 0;
}

D.百战名

题意

给出一元二次方程 $ax^2+bx+c=0$ 以及 $n,p$,求该方程 $(x_1^n+x_2^n)\mod p$ 的值。

思路

考虑递推,需要推式子。

设 $f_i=x_1^i+x_2^i$,则有 $f_0=2,f_1=x_1+x_2$

$(x_1^{i-1}+x_2^{i-1})(x_1+x_2)=x_1^i+x_2^i+x_1^{i-1}x_2+x_1x_2^{i-1}=(x_1^i+x_2^i)+x_1x_2(x_1^{i-2}+x_2^{i-2})$

即 $(x_1+x_2)f_{i-1}=f_i+x_1x_2f_{i-2}$

整理得 $f_i=(x_i+x_2)f_{i-1}-x_1x_2f_{i-2}$

发现 $f_i$ 只与 $f_{i-1},f_{i-2}$ 有关,考虑构造矩阵加速递推。

因此有 $\begin{pmatrix} f_i & f_{i-1} \end{pmatrix} =\begin{pmatrix} f_{i-1} & f_{i-2} \end{pmatrix} \begin{pmatrix} x_1+x_2 & 1 \ -x_1x_2 & 0 \end{pmatrix}$,

即有$\begin{pmatrix} f_n & f_{n-1} \end{pmatrix} =\begin{pmatrix} f_{1} & f_{0} \end{pmatrix} \begin{pmatrix} x_1+x_2 & 1 \ -x_1x_2 & 0 \end{pmatrix}^{n-1}$

使用矩阵快速幂即可,要注意负数的取模。

代码

#include<iostream>
#define int long long
using namespace std;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') { s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int a,b,c,p,n;
struct mat
{
	int s[2][2];
	void unit()
	{
		s[0][0]=s[1][1]=1,s[0][1]=s[1][0]=0;
	}
	void init()
	{
		s[0][0]=s[1][1]=s[0][1]=s[1][0]=0;
	}\/\/矩阵初始化 
	mat operator * (const mat &b) const
	{
		mat res; res.init();
		for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
			res.s[i][j]=(res.s[i][j]+s[i][k]*b.s[k][j]%p)%p;
		return res;
	}\/\/定义矩阵乘法 
}ini,base;
mat mpo(mat a,int b)
{
	mat res; res.unit();
	while(b)
	{
		if(b&1) res=res*a;
		a=a*a,b>>=1;
	}
	return res;
}\/\/矩阵快速幂 
int po(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1) res=res*a%p;
		a=a*a%p,b>>=1;
	}
	return res;
}
int inv(int a)
{
	return po(a,p-2);
}\/\/快速幂求逆元 
signed main()
{
	a=read(),b=read(),c=read(),p=read(),n=read();
	a%=p,b%=p,c%=p;
	int add=(-b*inv(a)%p+p)%p,cdot=c*inv(a)%p;
	ini.init();
	ini.s[0][0]=add,ini.s[0][1]=2;\/\/初始值 
	if(n==0) cout<<ini.s[0][1];
	else if(n==1) cout<<ini.s[0][0];
	else
	{
		base.s[0][0]=add,base.s[1][0]=-cdot,base.s[0][1]=1,base.s[1][1]=0;
		mat res=ini*mpo(base,n-1);
		cout<<(res.s[0][0]%p+p)%p;
	}
	return 0;
}

G.十万家

题意

每次给定整数 $N$,求使各位数字均不同且和为 $N$ 的最小值,无解输出 $-1$。

思路

显然,在数中有非前导 $0$ 会比去掉该 $0$ 后的值大,而数位和不变。因此答案中不含 $0$,这里只需考虑 $1$ 到 $9$。

还是显然,因为 $\sum_{i=1}^9 i=45$,所以 $45$ 及以下的数均可凑出解,而 $45$ 以上的数不可能凑出解。

那么对于有解的 $N$,首先要尽量使其位数少,因此从 $9$ 到 $1$ 依次枚举。其次要让高位尽量小,所以把 $9$ 到 $1$ 从低位向高位依次填即可。

代码

#include<iostream>
#define int long long
using namespace std;
const int N=2e5+10;
const int K=60+10;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') { s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
signed main()
{
	int T=read();
	while(T--)
	{
		int n=read();
		if(n>45) cout<<"-1"<<"\n";\/\/判无解 
		else for(int i=9;i>=1;i--)
		{
			if(n-i<=0)
			{
				cout<<n;
				for(int j=i+1;j<=9;j++) cout<<j;
				cout<<"\n";
				break; 
			}\/\/求完后输出 
			n-=i;
		}
	}
	return 0;
}

H.百万师

题意

给出长度为 $N$ 的数组 $A$,每个数可以 $+1,-1$ 或不变,问是否能将其变为等差数列。若能,求最小修改次数,修改次数最小的方案数和这些方案。

思路

考虑找等差数列中的首项 $A_1$ 和公差 $k$,发现对于前两个数 $A_1,A_2$ 来说,每个数有三种情况,因此两个数共有九种不同的首项和公差

那么对于每一种可能的情况,暴力扫一遍整个数组,判断是否能够通过操作使其变为目标数列,再维护最小值和方案数即可。如此总时间复杂度为 $O(9N)$。

关于方案输出,只要记录每种方案的首项和公差,即可计算并输出该数列的每一项,同时也能按照要求排序。但是由于std中的枚举顺序为 $A_1$ 递增后 $A_2$ 递增,此处实际上不用排序。

代码

#include<iostream>
#include<algorithm>
#define int long long
using namespace std;
const int N=1e7+10; 
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') { s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
void write(int x)
{
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x\/10);
	putchar(x%10+'0');
}
int T,n,cnt,mn,a[N];\/\/cnt为方案数,mn为最小操作数
struct nod
{
	int st,gc;
}ans[10];\/\/st,gc分别为首项和公差 
bool cmp(nod a,nod b)
{
	if(a.st==b.st) return a.gc<b.gc;\/\/公差小的第二项就小 
	else return a.st<b.st;
} 
signed main()
{
	T=read();
	while(T--)
	{
		n=read(),cnt=0,mn=n+1;
		for(int i=1;i<=n;i++) a[i]=read();
		for(int i=-1;i<=1;i++) for(int j=-1;j<=1;j++)
		{
			int tc=0,k=a[2]+j-(a[1]+i);\/\/tc为本次次数,k为公差 
			if(i!=0) tc++;
			if(j!=0) tc++;\/\/前两项所需操作 
			for(int l=3;l<=n;l++) 
			{
				int bz=a[1]+i+k*(l-1);\/\/这一项需要达到的值 
				if(a[l]==bz) continue;\/\/相等不操作 
				if(a[l]>=bz-1&&a[l]<=bz+1) tc++;\/\/相差1则操作数+1 
				else
				{
					tc=-1;
					break;
				} \/\/无法操作使其为这个等差数列,退出 
			}
			if(tc==-1) continue;\/\/同上 
			else
			{
				if(tc==mn)
				{
					cnt++;
					ans[cnt]={a[1]+i,k};
				}\/\/若有更多种最优情况,增加方案数 
				else if(tc<mn)
				{
					mn=tc;
					cnt=1;
					ans[cnt]={a[1]+i,k};
				}\/\/更新最优情况 
			}
		}
		if(!cnt)
		{
			write(-1);
			putchar('\n'); putchar('\n');
			continue;
		}\/\/无解 
		sort(ans+1,ans+1+cnt,cmp);\/\/按要求排序 
		write(mn); putchar('\n');
		write(cnt); putchar('\n');
		for(int i=1;i<=cnt;i++)
		{
			for(int j=0;j<n;j++) write(ans[i].st+j*ans[i].gc),putchar(' ');
			putchar('\n');
		}
		putchar('\n');\/\/按题意输出 
	}
	return 0;
}

ABC350E 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-04-20 22:09:37

题意

有两种操作方式:第一种为把 $N$ 变为 $\lfloor \frac{N}{A}\rfloor$,代价为 $X$;第二种为把 $N$ 变为 $\lfloor \frac{N}{k}\rfloor$,代价为 $Y$,其中 $k$ 为掷骰子的结果,即从 $\{ 1,2,3,4,5,6\}$ 中等概率选取一个。求以最优方式操作时把 $N$ 变成 $0$ 的期望代价。

思路

设 $f_i$ 表示把 $i$ 变成 $0$ 的期望代价,显然有 $f_0=0$。

考虑状态转移。若使用第一种操作,则显然有 $f_i=X+f_{\lfloor \frac{N}{A}\rfloor}$。

若使用第二种操作,由于有六种等概率的情况,有 $f_i=Y+\frac{1}{6}\sum_{k=1}^6 f_{\lfloor \frac{N}{k}\rfloor}$,即$f_i=Y+\frac{1}{6}f_i+\frac{1}{6}\sum_{k=2}^6 f_{\lfloor \frac{N}{k}\rfloor}$。

移项得 $\frac{5}{6}f_i=Y+\frac{1}{6}\sum_{k=2}^6 f_{\lfloor \frac{N}{k}\rfloor}$,即 $f_i=\frac{6}{5}Y+\frac{1}{5}\sum_{k=2}^6 f_{\lfloor \frac{N}{k}\rfloor}$。

用以上两种方式中较小的代价转移即可,可以记忆化搜索,使用map记录状态。

代码

#include<iostream>
#include<map>
#define int long long
using namespace std;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') { s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,A,X,Y;
map <int,double> f;
map <int,bool> v;
double sol(int x)
{
	if(v[x]) return f[x];
	v[x]=1;
	if(!x) f[x]=0;
	else
	{
		double ta=sol(x\/A)+X,tb=Y*1.2;
		for(int i=2;i<=6;i++) tb+=sol(x\/i)\/5;
		f[x]=min(ta,tb);
	}
	return f[x];
}
signed main()
{
	n=read(),A=read(),X=read(),Y=read();
	printf("%.6lf",sol(n));
	return 0;
}

CF1957D 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-04-22 14:28:39

题意

定义 $f(l,r)$ 为 $a_l$ 到 $a_r$ 的异或和,三元组 $(x,y,z)$ 合法当且仅当 $1\le x\le y\le z\le n$,且 $f(x,y)\oplus f(y,z)>f(x,z)$,求合法三元组的个数。

思路

展开式子,得到 $f(x,y)\oplus f(y,z)=f(x,z)\oplus a_y$,考虑枚举每个 $i$ 作为 $y$ 的贡献,问题转化为求满足 $x\le y\le z$ 且 $f(x,z)\oplus a_y>f(x,z)$ 的二元组 $(x,z)$ 数量。

考虑 $f(x,z)$ 要怎样才能异或上 $a_y$ 后变大。发现若 $a_y$ 二进制下最高位为第 $k$ 位,则 $f(x,y)$ 的第 $k$ 位为 $0$ 即合法,否则不合法。

考虑证明,显然 $a_y$ 最高位的值一定大于 $\frac{a_y}{2}$,所以 $f(x,z)$ 异或 $a_y$ 后,第 $k$ 位增加的值一定比后面的位总共减少的值大,结果一定增加;反之也一样。

那么需要快速求出某一位为 $0$ 的 $f(x,z)$ 数量,考虑使用前缀和。设 $b_i$ 为 $a_1$ 至 $a_i$ 的异或和,那么 $f(x,z)=b_{x-1}\oplus b_z$。特别地,有 $b_0=0$。

如此 $f(x,z)$ 第 $k$ 位为 $0$ 就转化为 $b_{x-1}$ 和 $b_z$ 的第 $k$ 位相同。因此再设 $s_{i,j}$ 表示 $b_0$ 到 $b_i$ 中第 $j$ 位为 $1$ 的个数,这样用乘法原理即可快速求出合法的 $(x,z)$ 个数。

代码

#include<iostream>
#include<cmath>
#define int long long
using namespace std;
const int N=1e5+10;
const int K=30+10;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9') { s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int T,n,ans,a[N],b[N],s[N][K],h[N];\/\/h[i]表示a[i]的最高位,其余如上述 
signed main()
{
	T=read();
	while(T--)
	{
		n=read(),ans=0;
		for(int i=1;i<=n;i++) a[i]=read(),b[i]=b[i-1]^a[i],h[i]=floor(log2(a[i]));
		for(int i=1;i<=n;i++)
		{
			for(int j=0;j<=30;j++) s[i][j]=s[i-1][j];
			int ct=0;
			while(b[i])
			{
				if(b[i]&1) s[i][ct]++;
				b[i]>>=1,ct++;
			}
		}\/\/按思路维护各数组 
		for(int i=1;i<=n;i++) 
		{
			int ta=s[i-1][h[i]],tb=s[n][h[i]]-s[i-1][h[i]];\/\/b[x-1]与b[z]同为1的数量 
			int tc=i-ta,td=n-i+1-tb;\/\/同为0的数量,注意x=1时b[0]也要考虑进去 
			ans+=ta*tb+tc*td;\/\/统计方案数 
		}
		cout<<ans<<'\n';
	}
	return 0;
}

ABC340D 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-10 21:46:32

题意

游戏有 $n$ 个阶段,对于第 $i$ 个阶段,可以消耗 $A_i$ 秒开启第 $(i+1)$ 个阶段,也可以消耗 $B_i$ 秒开启第 $x_i$ 个阶段。初始只能进行第 $1$ 个阶段,求开启第 $n$ 个阶段需要的最短时间。

思路

本题考查图论建模和最短路。

由于 $A_i$ 和 $B_i$ 描述的都是阶段之间转移的代价,容易想到以阶段为点,阶段转移为边,所需时间为边权建图,则所求最短时间即为点 $1$ 到点 $n$ 的最短路。

则对于前 $(n-1)$ 个点中的点 $i$ ,均向点 $(i+1)$ 连一条边权为 $A_i$ 的有向边,向点 $X_i$ 连一条边权为 $B_i$ 的有向边。

之后用最短路算法跑单源最短路即可,Dijkstra算法可过。

代码

#include<iostream>
#include<vector>
#include<queue>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e5+10;
const int maxs=1e15;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; if(ch==EOF) return 0; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n;
struct edg
{
	int v,w;
};
vector <edg> edge[N];\/\/邻接表存边 
void add(int u,int v,int w)
{
	edg t;
	t.v=v;
	t.w=w;
	edge[u].push_back(t);
}\/\/加有向边 
int s[N];\/\/最短路长度 
bool v[N];\/\/是否已经计算完最短路 
priority_queue <pii > q;\/\/注意为大根堆,且按pair的first排序 
signed main()
{
	n=read();
	for(int i=1;i<n;i++)
	{
		int a=read(),b=read(),x=read(); 
		add(i,i+1,a);
		add(i,x,b);\/\/加边 
	} 
	for(int i=1;i<=n;i++)
		s[i]=maxs,v[i]=0;
	s[1]=0;
	q.push({0,1});\/\/Dijkstra初始化 
	while(!q.empty())
	{
		int u=q.top().second;
		q.pop();
		if(v[u])
			continue;
		v[u]=1;
		for(int i=0;i<edge[u].size();i++)
		{
			int ne=edge[u][i].v,tw=edge[u][i].w;
			if(s[ne]>s[u]+tw)
			{
				s[ne]=s[u]+tw;
				if(!v[ne])
					q.push({-s[ne],ne});\/\/取负,把大根堆用作小根堆 
			}
		}
	}\/\/Dijkstra
	cout<<s[n];
	return 0;
}

ABC340E 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-10 22:08:18

题意

有 $n$ 盒球(编号从 $0$ 开始),第 $(n-1)$ 个盒的下一个盒为第 $0$ 个,每盒初始有一些球。每次拿出一个盒内的所有球,从该盒开始不断向下一个盒放一个球,放完为止,求最终每盒内的球数。

思路

本题有显然的模拟解法,但 $A_i$ 过大,时间复杂度过高,无法通过本题。

我们发现时间复杂度的瓶颈在于模拟放球,此处可以使用线段树优化区间加操作。

(以下设取出的球数为 $S$)

当 $S$ 较大时,每个盒子至少会被加上 $\lfloor \frac{S}{n} \rfloor$ 个球,也就是至少会把 $n$ 个盒子全部放 $\lfloor \frac{S}{n} \rfloor$ 遍。

发现这里的多次放球可以转化为放多个球,便可以用一次区间加解决。

因此先对全部盒子进行一次区间加处理,同时令 $S$ 对 $n$ 取模,如此可以保证没有重复加球的盒子。

之后使用线段树进行区间加 $1$,最后输出答案即可。

此处需要注意剩余的区间可能会从第 $(n-1)$ 个盒子跳到第 $1$ 个盒子,需要特判成两个区间分别进行维护。

具体操作请看代码实现。

代码

注:本题需要实现区间修改和单点查询,但单点就是长度为 $1$ 的区间,因此笔者写了区间查询。

#include<iostream>
#define int long long
using namespace std;
const int N=2e5+10;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; if(ch==EOF) return 0; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,m,t=1;
int aa[N]; 
struct nod
{
	int w,l,r,tag;
}a[N*2];\/\/线段树记得开两倍空间 
void pushup(int u)
{
	a[u].w=a[a[u].l].w+a[a[u].r].w;
}
void pushdown(int u,int l,int r)
{
	int mid=(l+r)\/2,lc=a[u].l,rc=a[u].r;
	a[lc].tag+=a[u].tag;
	a[rc].tag+=a[u].tag;
	a[lc].w+=a[u].tag*(mid-l+1);
	a[rc].w+=a[u].tag*(r-mid);
	a[u].tag=0;
} 
void build(int u,int l,int r)
{
	if(l==r)
	{
		a[u].w=aa[l];
		return;
	}
	int mid=(l+r)\/2;
	a[u].l=++t;
	build(t,l,mid);
	a[u].r=++t;
	build(t,mid+1,r);
	pushup(u);
}\/\/建树 
void add(int u,int l,int r,int ll,int rr,int k)
{
	if(l>=ll&&r<=rr)
	{
		a[u].tag+=k;
		a[u].w+=k*(r-l+1);
		return;
	}
	pushdown(u,l,r);
	int mid=(l+r)\/2;
	if(ll<=mid)
		add(a[u].l,l,mid,ll,rr,k);
	if(rr>mid)
		add(a[u].r,mid+1,r,ll,rr,k);
	pushup(u);
}\/\/对ll-rr区间加 
int check(int u,int l,int r,int ll,int rr)
{
	if(l>=ll&&r<=rr)
		return a[u].w;
	int mid=(l+r)\/2,ans=0;
	pushdown(u,l,r);
	if(ll<=mid)
		ans+=check(a[u].l,l,mid,ll,rr);
	else
		ans+=check(a[u].r,mid+1,r,ll,rr);
	return ans;
}\/\/查询ll-rr区间和 
signed main()
{
	n=read(),m=read();
	for(int i=0;i<n;i++)
		aa[i]=read();
	build(1,0,n-1);
	for(int i=1;i<=m;i++)
	{
		int tu=read();
		int ts=check(1,0,n-1,tu,tu);\/\/取出的球数 
		add(1,0,n-1,tu,tu,-ts);\/\/取球操作 
		int sum=ts\/n;\/\/每个盒子放一遍的轮数 
		if(sum>0)
			add(1,0,n-1,0,n-1,sum);
		ts%=n;\/\/处理该情况
		if(ts==0)
			continue;\/\/如果已经放完了就结束 
		if(tu+ts>n-1)
		{
			if(tu!=n-1)
				add(1,0,n-1,tu+1,n-1,1);
			ts-=(n-1-tu),tu=-1;
		}\/\/若出现两个区间则先将以(n-1)为结尾的区间处理完,转到另一个以1开头的区间 
		tu++;
		add(1,0,n-1,tu,tu+ts-1,1);\/\/对剩余区间操作 
	}
	for(int i=0;i<n;i++)
		cout<<check(1,0,n-1,i,i)<<' '; \/\/输出 
	return 0;
}

ABC340C 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-11 00:16:54

题意

删除一个整数 $x$ 要花费 $x$ 的代价,并会产生 $\lfloor \frac{x}{2}\rfloor$ 和 $\lceil \frac{x}{2} \rceil$ 两个新数。现在有一个整数 $N$,求使最后剩下的数均小于 $2$ 需花费的代价。

思路

若设 $f_x$ 为处理 $x$ 需花费的代价,令 $a=\lfloor \frac{x}{2}\rfloor,b=\lceil \frac{x}{2} \rceil$,则据题意易得 $f_1=0,f_x=f_a+f_b+x(x\ge2)$。

显然可以用递归解决。

但是直接暴力递归时间会炸

所以考虑使用记忆化搜索,用map记录下每个数所需的代价。

时间复杂度应为 $O(\log N)$ 级别,可以通过本题。

代码

#include<iostream>
#include<map>
#define int long long
using namespace std;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; if(ch==EOF) return 0; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,ans=0;
map <int,int> ma;
int erase(int x)
{
	if(x<2)
		return 0;
	if(!ma.count(x))
	{
		if(x%2==0)
			ma[x]=erase(x\/2)*2+x;
		else
			ma[x]=erase(x\/2)+erase(x\/2+1)+x;\/\/简单分类讨论+记忆化
	}
	return ma[x];
}
signed main()
{
	n=read();
	cout<<erase(n);
	return 0;
}

坦夫稼轩(互测赛)题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-14 01:17:18

A.永遇乐

出题人谢罪:

本题原本的出题意图是考察分层图和虚点,但是发现两个都不需要……现在看来是 $k_1$ 给小了。

所以能跑过就行,大家能写出做法也很强,下面是参考做法,仅供参考

题意

求在无向图上从 $1$ 号节点出发,在 $k_1$ 个点中任意一个停留后到达 $k_2$ 个点中任意一个的最短路。

思路

考虑把在点上停留也转化成边,自然想到分层图。

建两层图,每层内连无向边,再把 $k_1$ 个点由第一层向第二层连权值为 $a_i$ 的有向边。答案即为以第二层 $k_2$ 个点为终点的最短路的最小值。

为了快速求出答案,可以将第二层的 $k_2$ 个点向虚点(代码中为 $0$ 号点)连边,这样最终答案即为 $1$ 到 $0$ 的最短路。

代码

#include<iostream>
#include<vector>
#include<queue>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+10;
const int maxs=1e15;
int read()
{
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
    return s*w;
}
int n,m,ka,kb,d[N*2];
priority_queue <pii > q; 
bool f[N*2];\/\/因为建了两层图,所以要开两倍空间
struct edg
{
	int v,w;
};
vector <edg> edge[N*2];\/\/同上
void add(int u,int v,int w)
{
	edg t;
	t.w=w,t.v=v;
	edge[u].push_back(t);
}\/\/加边操作
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),w=read();
		add(u,v,w),add(v,u,w);
		add(n+u,n+v,w),add(n+v,n+u,w);\/\/层内建边
	}
	ka=read(),kb=read();
	for(int i=1;i<=ka;i++)
	{
		int t=read(),ta=read();
		add(t,n+t,ta);\/\/层间建边,即为停留的代价
	}
	for(int i=1;i<=kb;i++)
	{
		int t=read();
		add(n+t,0,0);\/\/连向虚点
	}
	for(int i=0;i<=2*n;i++) d[i]=maxs,f[i]=0;
	d[1]=0;\/\/初始化
	q.push({0,1});
	while(!q.empty())
	{
		int u=q.top().second; q.pop();
		if(f[u]) continue;
		f[u]=1;
		for(int i=0;i<edge[u].size();i++)
		{
			int ne=edge[u][i].v,tw=edge[u][i].w;
			if(d[ne]>d[u]+tw)
			{
				d[ne]=d[u]+tw;
				q.push({-d[ne],ne});
			}
		}
	}\/\/Dijkstra
	cout<<d[0];
	return 0;
}

B.贺新郎

题意

给出 $n$ 个数对 $(a_i,b_i)$,求一种排列使 $\sum_{i=1}^{n-1}\left| a_i-a_{i+1}\right|+\left| b_i-b_{i+1}\right|$ 最大。

思路

看数据范围显然是状压dp,这里可以把题意抽象成在平面直角坐标系中有 $n$ 个点,两点之间距离为曼哈顿距离,本题便转化成了从任意点开始到任意点结束的旅行商问题(可参照P1433),只不过本题要求最大值(当然这里不转化也是状压dp)。

代码

#include<iostream>
#include<cmath> 
#define int long long
using namespace std;
const int N=18+10;
const int M=3e5+10;
int read()
{
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
    return s*w;
}
int n,ans=-1,x[N],y[N];
int s[N][N];\/\/存储跳跃度
int f[N][M];\/\/f[i][j]表示以i结尾,j状态可达到的最大效果值
int dis(int a,int b)
{
	return abs(x[a]-x[b])+abs(y[a]-y[b]);
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) x[i]=read();
	for(int i=1;i<=n;i++) y[i]=read();
	for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++)
		s[i][j]=dis(i,j),s[j][i]=s[i][j];
	for(int i=1;i<=n;i++) f[i][1<<(i-1)]=0;\/\/可从任意部分开始,但其实在这份代码里这个初始化没用
	for(int k=1;k<(1<<n);k++) for(int i=1;i<=n;i++)\/\/枚举状态和最终点
	{ 
		if(k&(1<<(i-1))==0)	continue;
		for(int j=1;j<=n;j++)\/\/枚举上一个点
		{
			if(i==j||(k&(1<<(j-1)))==0) continue;
			f[i][k]=max(f[i][k],f[j][k-(1<<(i-1))]+s[j][i]);\/\/转移
		}
	}
	for(int i=0;i<n;i++) ans=max(ans,f[i][(1<<n)-1]);\/\/可在任意部分结束,记录答案
	cout<<ans;
	return 0;
}

C.青玉案

没想到吧,这题是原题P4042,是出题人在qbxt做到的题) (虽然原题是紫,但个人觉得大概是蓝)

题意

每个烟花可以公开放,这样会得到若干个烟花并增加热闹值;也可以自己放,只会增加热闹值。求最小热闹值。

思路

若设 $f_i$ 为把第 $i$ 种烟花及其后产生的新烟花全部放完的最小代价,则显然有 $f_i=min(b_i,a_i+\sum_{j=1} ^{s_i}f_{v_j})$,其中 $v$ 是存储公开放这个烟花所产生的新烟花的数组。

显然仅当 $f_{v_j}$ 均小于 $f_i$ 时,$f_i$ 才可能从后边的式子转移。因此类似于最短路算法,把所有烟花放入堆中,按所需代价从小到大处理即可,具体看代码。

(其实好像还有一点拓扑的思想,即只有一种烟花会产生的所有烟花都处理完,才能处理该烟花对应式子的第二项)

代码

此处代码中的 $f_i$ 仅维护了上式的第二项,即 $a_i+\sum_{j=1} ^{s_i}f_{v_j}$。上式第一项由初始加入堆的元素维护,把答案存到了 ${ans}_i$ 中。

#include<iostream>
#include<vector>
#include<queue>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e5+10;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,a[N],b[N],s[N],f[N],ans[N];
bool v[N];
vector <int> edge[N];\/\/存储产生烟花情况
priority_queue <pii > q;
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read(),b[i]=read(),s[i]=read();
		for(int j=1;j<=s[i];j++)
		{
			int t=read();
			edge[t].push_back(i);\/\/由产生的烟花连向该烟花
		}
		q.push({-b[i],i});\/\/初始把每个烟花都以直接放完的代价放入队列
		f[i]=a[i];\/\/初值设为公开燃放的代价
	}
	while(!q.empty())
	{
		int u=q.top().second,w=-q.top().first; q.pop();
		if(v[u]) continue;
		v[u]=1,ans[u]=w;\/\/记录答案
		for(int i=0;i<edge[u].size();i++)
		{
			int ne=edge[u][i];
			if(v[ne]||f[ne]>b[ne]) continue;
			f[ne]+=w;\/\/此处就会将所有f_v_j加到f中
			s[ne]--;\/\/记录产生烟花中未处理的数量
			if(s[ne]==0) q.push({-f[ne],ne});\/\/若全部处理完则再加入堆
		}
	}\/\/Dijkstra思想
	cout<<ans[1];
	return 0;
}

D.破阵子

(类似于P1395,由于把树这个信息坑人地藏了起来,同时点有点权,评了绿)

题意

给定一棵树(因为每两个地区之间有且仅有一条路线,可以确定为一棵树,即保证 $m=n-1$),每将地区上的兵力移到直接连接的地区上时,就会消耗兵力数的代价,求集中所有兵力到同一地区的最小代价。

思路

既然说了是树那显然是树形dp了吧

典型换根dp,先钦定 $1$ 为根,设 ${cnt}_i$ 为子树中的兵力数,${sum}_i$ 为子树中所有兵力转移到子树根节点的代价,$j$ 为 $i$ 的子节点,则显然有 ${cnt}_i=a_i+\sum cnt_j$,${sum}_i=\sum (sum_j+cnt_j)$,一遍dfs即可处理完。

然后再计算以每个点为集中点的总代价 ${ans}_i$,其中 ${ans}_1={sum}_1$。若设总兵力数为 $tot$,$v$ 为原树中 $u$ 的子节点,则有:

${ans}_v={ans}_u-{cnt}_v+({tot}-{cnt}_v)={ans}_u+{tot}-2\times {cnt}_v$

这样再dfs一遍同时更新答案最小值即可。

代码

#include<iostream>
#include<vector>
#define int long long
using namespace std;
const int N=2e5+10;
const int maxs=1e15;
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1;  ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,m,tot=0; 
vector <int> edge[N];
int a[N],sum[N],cnt[N],ans[N],minn=maxs;
void dfs(int u,int fat)
{
	sum[u]=0,cnt[u]=a[u];
	for(int i=0;i<edge[u].size();i++)
	{
		int ne=edge[u][i];
		if(ne==fat) continue;
		dfs(ne,u);
		cnt[u]+=cnt[ne],sum[u]+=(sum[ne]+cnt[ne]);
	}
}\/\/第一遍dfs记录cnt和sum
void dfsb(int u,int fat)
{
	minn=min(minn,ans[u]);
	for(int i=0;i<edge[u].size();i++)
	{
		int ne=edge[u][i];
		if(ne==fat) continue;
		ans[ne]=ans[u]+tot-cnt[ne]*2;
		dfsb(ne,u);
	}
}\/\/第二遍dfs记录答案
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read(),tot+=a[i];
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		edge[u].push_back(v),edge[v].push_back(u);
	}
	dfs(1,0);
	ans[1]=sum[1];
	dfsb(1,0);
	cout<<minn; 
	return 0;
}

E.水调歌头

(分组背包裸题,可参照P1757

(这题应该是最简单的吧,板子是橙,由于需要自己处理组别,所以我评了黄)

闲聊

这里不讲题意之类的了,只说下部分分,$20\%$ 的数据给的是不会滚动数组的(话说真的有人不会吗),$30\%$ 的数据显然每件事单独为一组,01背包即可,同时这个Subtask中由于还有 $y_i$ 和 $m_i$ 的大小限制,实际有 $n\leq816$ 。

(其实此题 $y_i$ 的限制是辛弃疾的生卒年)

std代码

#include<iostream>
using namespace std;
const int N=4e3+10;
const int K=4e5+10;
const int M=9e2+10;\/\/最多只有(1207-1140+1)*12=816个组
int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') w=-1; ch=getchar();}
	while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar();}
	return s*w;
}
int n,k,l=1e9,r=-1e9; \/\/这里记录的是下标最大和下标最小的组
int cnt[M],w[M][N],v[M][N],f[K];
int main()
{
	n=read(),k=read();
	for(int i=1;i<=n;i++)
	{
		int y=read(),m=read(),tw=read(),tv=read();
		int gr=(y-1140)*12+m;\/\/这里转化一下标号,方便存储
        cnt[gr]++,l=min(l,gr),r=max(r,gr);\/\/更新组数的范围
		w[gr][cnt[gr]]=tw,v[gr][cnt[gr]]=tv;\/\/分组存储
	}
	for(int i=l;i<=r;i++) for(int j=k;j>=0;j--)\/\/枚举每个组和最终的容量
	{
		bool tf=true;
		for(int tk=1;tk<=cnt[i];tk++) if(j-w[i][tk]>=0)\/\/枚举每件物品
				tf=false,f[j]=max(f[j],f[j-w[i][tk]]+v[i][tk]);
		if(tf) break;\/\/若所有物品都无法更新,则跳出循环(这里不剪枝会慢很多,但能过) 
	}
	cout<<f[k];
	return 0;
} 

【学习记录】24.02.15 图论

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-15 17:12:33

图论

最短路

CF715B Complete The Graph

二分或Dij思想填数。

P2149 [SDOI2009] Elaxia的路线

Dij预处理+DAG拓扑排序。

P5304 [GXOI/GZOI2019] 旅行者

二进制分组建虚点+Dij。

P2371 [国家集训队] 墨墨的等式

同余最短路。

CF786B Legacy

线段树优化建图 Dij。

P4001 [ICPC-Beijing 2006] 狼抓兔子

虚点建图最短路/最大流最小割。

CF1163F Indecisive Taxi Fee

黑题放弃...

学长讲了思路,分类讨论+Dij处理三种+线段树优化处理最后一种,最后一种不太明白。

LCA

P3398 仓鼠找 sugar

LCA+判断是否在路径上。

P4281 [AHOI2008] 紧急集合 / 聚会

$3$ 次LCA,推出答案为较深点,再求解。

加强版

建虚树,后边没咋听懂。

生成树

P4180 [BJWC2010] 严格次小生成树

最小生成树上 倍增求LCA和路径最值。

CF1416D Graph and Queries

没听。

拓扑排序

P7831 [CCO2021] Travelling Merchant

类似于反图拓扑排序,没太听懂。

差分约束

P7624 [AHOI2021初中组] 地铁

环上差分约束,需要分类讨论。

Err0r

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-15 20:49:02

模拟赛

25.07-SDSC

  • D1T1 corner case 变量写错挂五分。
  • D1T4 时间分配失误,导致忘了判不带修的情况。
  • D2T1 想麻烦了,写后缀数组 + 笛卡尔树上去,浪费了不少时间。
  • D2T3 没有思考算法正确性,在区间不变时被卡掉。
  • D3T2 数组开太多 MLE,同时细节写错挂分。
  • D4T3 忘判无解挂分。
  • D5T2 删调试把输出删了,爆零。
  • D7T1 贪心结论显然假了,挂分。
  • D7T3 暴力写挂挂分。

校内模拟

  • 25.08.20 T2 注意到结论后没有多加思考,导致写法复杂且容易出错。
  • 25.09.01 multiset 用成 set,挂分。

25.09-LCA

  • 09.25 T1 忘判前导零,挂分。
  • 10.02 T3 点双挂了调试时间过长,T4 没时间暴力。
  • 10.09 T4 忘看空间限制导致爆掉,挂分。
  • 10.20 T1.T3 corner 漏了,挂分。
  • 10.23 T3 Dijkstra 堆开错了,挂分。

语法问题

  • 不声明 vector 头文件不会报错,但会 CE(CF632D)
  • lower_bound 返回值是指针,直接减数组下标得到的值不能直接取 $\min$,可能会 CE(LCA-1108T3)
  • 关闭流同步的 cin/cout 不能与快读同时使用(ABC354G),不能与 scanf 同时使用(P10534)
  • scanf 读入字符比较鬼畜,最好别(LCA-1113T2)
  • __int128 注意强转类型,防止溢出(P10389)
  • 不开 #define int long long 后注意区分两个 inf(P2371)
  • pow 和 sqrt 均是浮点数运算,有精度问题(ABC361F)
  • 赋值 $inf=10^{18}+10$ 的话会因为浮点数精度变成 $10^{18}$,导致边界问题(T386386)
  • 直接交换两个数组复杂度为 $O(数组大小)$,用 vector 可 $O(1)$ 交换(CF1983C)
  • vector 常数大,存 $10^6$ 级别的元素要谨慎(QBXT24.11-R11T4)
  • 线下评测时会记录静态空间使用,程序使用空间应尽可能不超过一半(SDSC-2024-D3T1,2025-D3T2)
  • 排序的比较函数必须保证单向合法,不能出现两个元素相等(U468784)
  • 元素可重时用 multiset 而不是 set,删除单个元素需要使用 find(ABC170E,P12664)
  • 使用 set 的指针时,若 set 有变化指针就会失效,若继续使用是 UB,会运行错误(P11933)
  • 函数调用常数较大,取模可以改成 define(MX24.12.08T1)
  • vector 进行 resize 后不会清空为 $0$,需要手动清空(CF1439B)
  • 嵌套 pair 时无法比较,不能直接放到优先队列或 set 里(P9370)
  • int 右移超过 $31$ 位时可能出现问题(CF2118C)
  • 二维数组要按照访问顺序尽量让寻址连续,可以减小不少常数(P5046)

细节错误

  • 函数返回值类型写错(P7706)
  • 变量写错(P7831,CF1167E, T386391),数组和数组大小混用(P7913.CF1184E3),映射和原数混用(P2704)
  • 忘记特判 $0$,尤其是除以 $0$(P10730,P10469)
  • $i\ge0$ 和 $!i$ 用错(P5829)
  • 所有数据均要取模,包括初始值大于模数的数(P10511,24-ZR-D7T3)
  • 注意 $l<r$ 等判断要在取模之前(Gym104053F)
  • 数组开小(ABC306F,ABC364B.C, P3648),尤其是图论的边数可能是 $n+m$ 或 $2m$(P7624),手写栈 / 队列时要注意极限情况,尤其是队列要开到入队次数(T701837)
  • 多测清空(P3952,CF1983E),多测在中途返回值后清空(P3385),多测换行(24.09.27T2)
  • 多测要读入完再特判(ABC433E)
  • 忘开或没开全 long long(SDSC D2T4,P8392)
  • 输入后再预处理(CF482C)
  • 赋初值或清空时要注意清完,尤其注意更换下标后的最大值(CF2118D1)
  • 最值的初值要赋为无穷,$0$ 会导致漏情况(SGU482)
  • inf 赋值要略大于 $10^9$,否则可能会出边界问题(ABC393F)
  • 初始化变量时注意不同的初值,避免将所有值错误地赋成相同(P11046)
  • 极限数据特判(24.10.19T1,P11747)
  • $2^{63}$ 会爆 long long,需要 unsigned long long(P11747)
  • 求前若干大时整体后移要倒序枚举(ABC394F)
  • 二分时若 $r$ 初值是 $2\times 10^9$ 级别,在 $\frac{l+r}2$ 时可能爆 int(ABC395F)

算法实现

数据结构

  • 线段树开四倍空间(P3919P5787),写 pushdown(P1712,T392687),不要在线段树的叶子节点 pushup(P8334,P7712)
  • 吉司机线段树 pushdown 时,判断是否存在最大不能以目前父亲节点最大值判断,而要用左右儿子最大值,因为父亲节点可能已被更新过(P6242)
  • 线段树维护历史和时,历史和标记同样需要使用一次函数,尤其是吉司机线段树进行最值操作时的标记(LCA-09.25-T5)
  • Splay 的删除操作删根节点时进行了 Splay 操作,根节点会改变,需要在 Splay 前记录原来的根(P6136)
  • Splay 写文艺平衡树时要注意找到目前树上的第 $l,r+2$ 个节点操作,且需要将两个点到根的路径均 pushdown 一遍,可以在 kth 中实现(P3391)
  • 平衡树求第 $k$ 小时要注意根节点上有一个值,递归进右子树时要减去(P3369,P3391)
  • 动态开点线段树空间常数小,时间常数大(P1712)
  • 动态开点线段树节点个数初始化为 $1$(CF1741F,CF2030D)
  • 可持久化线段树修改一定要开新点(HT-S25.09.12)
  • 前缀后缀询问可以使用树状数组,常数远小于线段树(CSP2024T3)
  • 开桶注意数据规模,可能要开两倍(ABC282D)
  • 01Trie 中所有数组都要开 $n\log n$ 大小(P10853)
  • Trie 在插入时要注意维护根节点信息,如子树大小等(24-ZR-D7T3)
  • 并查集的路径压缩需要保证整条路径上全部压完,而不是只修改当前点的 $fa$(U471568)
  • 序列并查集注意预处理边界(P2391)
  • 可撤销并查集撤销时要用栈维护,顺序不能错(P5443)
  • ST 表等倍增在预处理时需要升序依次处理(ABC370F),树上倍增需要从根到叶子依次处理(Gym103446H)
  • LCT 中 rotate 时要注意各语句先后顺序,如 $y$ 是否为根的判断要在最前,$c_{x,!fx}$ 的使用在更新之前(P3690)
  • LCT 中 split,access 修改前,findroot 前后,单点值修改前均需 splay 至根(P3690)
  • LCT 中 rotate,access,cut 后均需 pushup,splay,findroot 前均需 pushdown(P3690)
  • set 取出某区间内元素时,取到 $x>r$ 要记得放回去(HT-P5707)
  • 线性基求第 $k$ 小值时要注意顺序,从高位到低位做(LCA-1113T3)

DP

  • 询问值较小而实际值较大时,把大于最大询问值的实际值压到询问值,再加入状态(P8548)
  • 分组背包先处理容量为 $0$ 的物品,防止计重(P1782)
  • 多重背包二进制优化是先保证最低的连续位全有,最后剩余的放在一个物品,保证所有数均能构成(P1782)
  • 换根 DP 时注意维护全部 DP 值,包括父节点和子节点,以及所有 DP 数组(CF627D)
  • 换根 DP 等 DFS 时注意全局变量不要重复使用,可能会替换掉有用信息,需要先用完再向下递归(HT-NOI077T2)
  • 斜率优化弹队尾时是直接弹出队尾,而不是删掉倒数第二个元素(P3648)

图论

  • 建超级源点之后点数为 $n+1$(P5960)
  • 链式前向星建边时反向边异或值为 $1$ 需要从 $0$ 或 $2$ 开始标号(P4043)
  • Dinic 算法要加当前弧优化以保证复杂度(LCA-XIAN11)
  • Dinic 算法中 dfs 时若到汇点返回值为 $flow$(P2764)
  • Dinic 算法中若 $r=f$ 时要退出循环,且要写在循环内,否则当前弧优化会假(QOJ143)
  • Dinic 求费用流时 dfs 要打标记,否则会由于零环产生死循环(P4014,P4016)
  • 求 LCA 跳祖先时 $k$ 从大到小枚举,且要枚举到 $0$(P3398,CF1184E3)
  • Dijkstra 开小根堆(LCA10.23T3)
  • Kruskal 生成树有两倍点数,若 ST 表快速求 LCA 也需要两倍大小,上界不能错(LOJ137)
  • Tarjan 求割边时判定条件是 ${low_v>{dfn}_u}$,而非 $low_u$(ABC375G)
  • Tarjan 求割点时注意特判根节点,特判条件是搜索树中的度数大于 $1$,而不是原图度数(QOJ996)
  • Tarjan 求割点只能用未遍历过的 $v$ 点判断,即其搜索树上的子节点(P3388)
  • Tarjan 求点双时弹栈直到弹出 $v$,而非弹出 $u$(LCA-25.10.02T4)
  • 缩点之后求度数之类要判 $u,v$ 是否在同一个连通分量内(MX-S10-B)
  • 基环树上拓扑排序求环后只有与环距离为 $1$ 的点 $r=1$,其他不在环上的点 $r=0$,不能直接用作标记(P2607)
  • 树链剖分时先 dfs 重儿子(T392687)
  • 树链剖分 + 多测时注意清空 $son$ 数组(CF2063E)
  • 树上把边权放到子节点上取路径信息时,注意 LCA 上的信息不能取(P4180)
  • 点分治搜索时注意判断 $vis$ 标记是否访问,否则复杂度会假(P3806)
  • 分层图求最短路时注意若层间的边有负贡献,需要以层数为第一关键字排序,否则会由于负边使得 Dijkstra 答案错误(P9370)
  • 01 bfs 的 vis 标记必须在出队的时候打。如果在入队的时候打,就可能有某个位置,本应该在之后某个时刻放入队首,却被放入队尾,导致比答案多 1(P3645)
  • 2-SAT 建图时必须建逆否命题,否则是错的(CF2147C)
  • 差分约束的限制和跑的最短 / 长路要对应(P5960)

字符串

  • 多测处理 char 数组时如果把 $0$ 下标暴力改成 $1$ 下标,会导致终止符丢失并接上前面数据的后面部分,导致出错(LOJ10043)
  • Z 函数中要初始化 $z_1=m$ 并从 $i=2$ 开始处理,还要记得更新 $l,r$,否则复杂度错误(CF1598G,P5410,P7114,QOJ786)
  • manachar 向原串中插入特殊字符时开头结尾都要加,可以方便讨论(QOJ787)
  • manachar 时单个字符为 $0$ 和为 $1$ 不要混用(P3805)
  • manachar 时 $f_i\leftarrow f_{l+r-i}$ 时要对 $r-i+1$ 取 $\min$,不然是假的(P3805)
  • AC 自动机建立时要将深度为 $1$ 的 $x$ 的 $fail_x$ 初始化为根节点(HT-P5637)

数学

  • 写 BSGS 等数论算法时可能有多个模数,变量不要用混(25.06-三轮省集 D6T2)
  • 拉格朗日插值求系数时若有 $x_i=0$ 且顺着求除法,需要特判掉分母为 $0$ 的除法(LCA-09.29-H)

其他算法

  • 二分的上下边界分析(CF1976C,P10730)
  • set 常数较大,可以使用可删堆代替「STL 性能陷阱」(25.02-MX-D3T1)
  • 整体二分时若可差分,可在询问上修改,而不把 $[l,mid]$ 内的操作反复执行,常数会减小(P1527)

【学习记录】24.02.16 数据结构

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-02-16 08:55:54

数据结构

线性数据结构

P6033 [NOIP2004 提高组] 合并果子 加强版

两个队列,一个存原数,另一个存合并后的数,每次取出两个队头比较,选出大的。初始桶排插入队列。

P2827 [NOIP2016 提高组] 蚯蚓

三个队列,分别存原数和分开的两部分。变长用 $Delta$ 维护,每次把 $Delta$ 加上 $q$,把不加的两条再减去 $q$

并查集

P1197 [JSOI2008] 星球大战

把删边操作存储下来,倒序加边,并查集维护。

(要注意读题,求的是****未被占领****的连通块个数)

P2391 白雪皑皑

维护并查集,让还没染色的点指向 $0$,其他点与其后的第一个没染色的点在同一个并查集内。

优化:以 $\log n$ 为大小分块(只能维护一维的并查集)(我不会)。

线段树

要注意维护的内容、标记的内容和高效的维护方式。

P3373 【模板】线段树 2

打过四遍的板子,注意懒惰标记pushdown的处理,还有要先乘再加。

P4513 小白逛公园

线段树维护区间最大子段和,还需要维护每个区间的最大前缀和、最大后缀和以及区间和。维护时划分中线,$pre$、$ans$ 和 $suf$ 都可以在一边或在两边。

P7706 「Wdsr-2.7」文文的摄影布置

$ans$ 三元组有四种情况,在左右区间或仅一个在左右区间。

因此要维护区间内一个差的最大值,维护两种此类二元组时也要分成在区间同一侧和两侧的情况讨论。

(多元组可以分成多个低元组维护,可能用背包)

P1471 方差

做过,当时是因为一个double写成int调了一个小时

推式子以后维护区间和以及区间平方和求解即可。

P6327 区间加区间 sin 和

维护区间和,区间 $sin$ 和以及区间 $cos$ 和,用和差角公式(我没学)维护求解。

P5278 算术天才⑨与等差数列

有以下条件:

  • 最大值和最小值之差等于公差乘项数-1;
  • 所有数模 $k$ 相等(用区间gcd维护)
  • 无重复数字(用 $pre_i$ 表示前面第一个与它相同的来判断,因此对每个值维护一个set存储它出现的下标,用lower_bound)

反正没听懂

P6617 查找 Search

没听。

Codechef DGCD(弱化版)

难点在标记对信息的修改。

将原来 $a$ 数组的gcd转化为差分数组的gcd, 则转化成单点修改+区间gcd,方便处理。

T367829 软萌甜心小仙女

结论:答案长度不大于 $3$(因为大于 $3$ 的分开后必有一段平均数不小于原来的平均数)

所以只需要考虑区间内长度为 $2$ 和 $3$ 的区间平均数即可,维护区间两个平均数以及区间首尾长度 $1$ 和 $2$ 的区间和即可。

CF280D k-Maximum Subsequence Sum

反悔贪心,黑题没听。

学长讲解:反悔贪心时取反,区间乘 $-1$,如此取 $k$ 次,每次更新答案。

乘 $-1$ 时相当于把最大变为最小,因此同时维护区间、前缀、后缀的最大最小值,乘 $-1$ 以后转换一下即可。听懂了还是不想写

P3215 [HNOI2011] 括号修复 / [JSOI2011]括号序列

  • 简化版,去掉swap操作。

在调题,没听。

P4036 [JSOI2008] 火星人

  • 简化版,去掉插入操作。

线段树维护区间哈希值,预处理出 $base$ 的若干次方。

询问时二分答案,变为判断两个区间是否相等

经典问题

题意:区间除以 $x$ 下取整,求区间和。

没咋听懂,好像要使数变为 $0$,原因和实现方式均没懂。

P4145 上帝造题的七分钟 2 / 花神游历各国

与上题一样,要使数变为 $1$,同样没听懂。

CF438D The Child and Sequence

看这个题的题解全明白了qwq。

对于某些线段树无法区间维护的运算,若对某个数只需操作很少的次数就不再改变(如除以一个数、取模的 $\log n$ 次和开平方的 $\log \log n$ 次),可以直接一down到底暴力修改,同时维护区间最大值。

当区间最大值已低于不会被改变的值(如除以的 $0$、开平方的 $1$、取模的模数) 时就不再操作,如此时间复杂度可以接受。

该性质称为复杂度均摊。

HDU 6315 Naive Operations

一个结论:

$\sum_{i=1}^{n} \frac{1}{i}=O(\log n)$

后边没听到,应该听了也不会

P9989 [Ynoi Easy Round 2023] TEST_69

没听。

LOJ504 ZQC的手办

也没听。

共 137 篇博客