2024.10&11 总结

图论

【Luogu P8428】 Pastiri

题目描述

给定一棵 \(N\) 点的树,点编号为 \(1\)\(N\),现在在 \(K\) 个点上有羊,你的任务是在树上分配一些牧羊人。

这些牧羊人很懒,只会看管离他最近的羊。当然如果有多个离他最近的羊,那么他会都看管。

当然,牧羊人可以和羊在同一个点上,但这样牧羊人只会看管这一个点上的那个羊。

求一种牧羊人的分配方案使得牧羊人总数最小,\(1 \le n \le 5 \times 10^5\)

解题思路

首先这题用树形 \(dp\) 是极其不现实的 ,数据大且信息不好表示。

我们可以考虑贪心:将所有羊按深度从大到小排序,每次选取一个深度最大的羊,在它的祖先中选取一个能够管到它的节点,在该节点上放牧羊人。

这个贪心的正确性是显然的,因为放的越高能照顾到的也越多,且由于深度最大,无需放到别的子树去管别的节点。

我们得到了一个 \(O(n^2)\) 的做法,考虑优化。

我们每次都需要花 \(O(n)\) 的时间复杂度来找深度最小且能照顾到自己的节点,并且将该节点能照顾到的所有节点都打上标记。

考虑优化查找,我们可以使用边定向,先求出每个节点 \(i\) 距离它最近的羊距离设为 \(dis_i\) ,然后对于所有 \(dis_u=dis_v+1\) 的边,我们在新图上建一条有向边 \((u,v)\) 表示点 \(u\) 能照顾点 \(v\) ,那我们就可以与处理出所有羊深度最小的且能处理它的节点了。

时间复杂度 \(O(n)\)

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,t[500005],dis[500005],deep[500005],f[500005],t1[500005],k,s;
bool v1[500005],v[500005];
vector<int> a[500005],a1[500005];
bool cmp1(int q,int w){return deep[q]>deep[w];}
void dfs1(int x,int y)
{
	if(!v1[x])dis[x]=n+1;
	deep[x]=deep[y]+1;
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs1(a[x][i],x);
		dis[x]=min(dis[x],dis[a[x][i]]+1);
	}
	return;
}
void dfs2(int x,int y,int z)
{
	dis[x]=min(dis[x],z);
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs2(a[x][i],x,dis[x]+1);
	}
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		if(dis[a[x][i]]==dis[x]+1)a1[a[x][i]].push_back(x);
		if(dis[x]==dis[a[x][i]]+1)a1[x].push_back(a[x][i]);
	}
	return;
}
void dfs4(int x,int y,int z)
{
	f[x]=z;
	for(int i=0;i<a1[x].size();i++)
	{
		if(a1[x][i]==y)continue;
		dfs4(a1[x][i],x,z);
	}
	return;
}
void dfs3(int x,int y)
{
	if(f[x]==n+1)dfs4(x,y,x);
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs3(a[x][i],x);
	}
	return;
}
void dfs5(int x,int y)
{
	v[x]=1;
	for(int i=0;i<a1[x].size();i++)
	{
		if(a1[x][i]==y||v[a1[x][i]])continue;
		dfs5(a1[x][i],x);
	}
	return;
}
int main()
{
	int x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		a[x].push_back(y),a[y].push_back(x);
	}
	for(int i=1;i<=m;i++)scanf("%d",&t[i]),v1[t[i]]=1;
	for(int i=1;i<=n;i++)f[i]=n+1;
	dfs1(1,0),dfs2(1,0,n+1),dfs3(1,0);
	sort(t+1,t+m+1,cmp1);
	for(int i=1;i<=m;i++)
	{
		if(v[t[i]])continue;
		s++,t1[++k]=f[t[i]];
		dfs5(f[t[i]],0);
	}
	sort(t1+1,t1+k+1);
	printf("%d\n",s);
	for(int i=1;i<=k;i++)printf("%d ",t1[i]);


  return 0;
}

动态规划

【Luogu P10681】 奇偶矩阵 Tablica

题目描述

考虑只包含 \(0\)\(1\)\(N\times M\) 矩阵 \(A\)

我们称满足以下条件的矩阵是好的:

  • \(\forall 1\le i\le N\)\(\displaystyle \sum_{j=1}^M A_{i,j}\in \{1,2\}\)
  • \(\forall 1\le j\le M\)\(\displaystyle \sum_{i=1}^N A_{i,j}\in \{1,2\}\)

求出 \(N\)\(M\) 列的好的矩阵的数量,对 \((10^9+7)\) 取模,\(1 \le n ,m \le 3000\)

解题思路

法一

由于矩阵只包含 \(0\)\(1\) ,我们把每个 \(1\) 的节点 \((i,j)\) 看成第 \(i\) 行所代表的点向第 \(j\) 行所代表的点连了一条边。

很明显,我们构造出了一个二分图,若这个图满足题目要求有两个条件,每个点不是独立的且每个连通块必须是一条链或一个环。

注意每个连通块在左右两边所占的个数最多只差 \(1\) ,参考 ABC180F ,做一个 \(n^2\)\(dp\) 即可。

法二

我们可以枚举有多少行、列和分别为 \(1\)\(2\) ,设有 \(a\) 行的和为 \(1\)\(b\) 行的和为 \(2\)\(c\) 列的和为 \(1\)\(d\) 列的和为 \(2\) ,满足 \(a+b=n,c+d=m,a+2b=c+2d\)

我们可以 \(O(n)\) 枚举 \(a,b,c,d\) ,考虑如何贡献答案。

首先给每行每列安排为 \(1\) 还是 \(2\) ,即乘上 \(C_{n}^{a} C_{m}^{b}\) ,然后考虑如何将每列的 \(1\) 分配到每行。

我们看成这样一个问题:有 \(c+2d\) 个小球,共有 \(m\) 种颜色,\(c\) 种颜色的小球每种各 \(1\) 个,\(d\) 种颜色的小球每种个 \(2\) 个,分成 \(n\) 个块,要求每块里面的球的颜色不能相同。

我们考虑将其排成一个序列,共有 \(\frac{(c+2d)!}{2^d}\) 种方案,然后按顺序分成块。

可能会有两种重复,第一种为出现 \({1,2}\)\({2,1}\) 的情况,这种直接除 \(2^b\) 即可。

第二种为出现 \({1,1}\) 的情况,这种情况我们需要容斥,考虑 \(t(0 \le t \le min(b,d))\)\(1,1\) 的集合,每次的答案即为 \((-1)^t A_{b}^{t}C_{d}^{t} \frac{(c+2d-2t)!}{2^{b+d-t}}\)

总答案 $ans=\sum C_{n}^a C_m^b \sum_{t=0}^{min(b,d)} (-1)^t A_{b}^t C_{d}^t \frac{(c+2d-2t)!}{2^{b+d-t}} $ 。

ABC273G 和这题差不多,只是每个点可能为 \(2\) ,枚举即可。

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long n,m,C[3005][3005],p[10005],p1[10005],a,b,c,d,s,f[10005];
long long poww(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1; 
	}
	return h;
}
int main()
{
	long long x,s1=0;
	scanf("%lld%lld",&n,&m),p[0]=p1[0]=f[0]=1;
	for(int i=1;i<=10000;i++)p[i]=p[i-1]*2%mod,p1[i]=poww(p[i],mod-2),f[i]=(f[i-1]*i)%mod;
	for(int i=0;i<=3000;i++)C[i][i]=C[i][0]=1;
	for(int i=1;i<=3000;i++)
		for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
	for(int i=0;i<=n;i++)
	{
		a=i,b=n-i,d=a+2*b-m,c=m-d,x=min(b,d),s1=0;
		if(c<0||d<0)continue;
		for(int j=0;j<=x;j++)
			s1=(s1+((j&1)?(-1):1)*(((C[b][j]*C[d][j]%mod)*f[j]%mod)*(f[c+2*d-2*j]*p1[b+d-j]%mod)%mod)%mod)%mod;
		s=(s+s1*(C[n][a]*C[m][c]%mod)%mod)%mod;
	}
	printf("%lld",(s+mod)%mod);


  return 0;
}

【Luogu P9197】摩天大楼

题目描述

将互不相同的 \(N\) 个整数 \(A_1, A_2, \cdots, A_N\) 按照一定顺序排列。

假设排列为 \(f_1, f_2, \cdots, f_N\),要求:\(| f_1 - f_2| + | f_2 - f_3| + \cdots + | f_{N-1} - f_N| \leq L\)

求满足题意的排列的方案数对 \(10^9+7\) 取模后的结果,\(1 \le n \le 100, 1 \le L \le 1000,1 \le A_i \le 1000\)

解题思路

题目中有绝对值不好处理,我们考虑将它放到平面直角坐标系中看一看,每个点为 \(i,f_i\) ,连成一条折线。

对于 $ |f_1 - f_2| + | f_2 - f_3| + \cdots + | f_{N-1} - f_N| $ 这个东西,在表格中我们可以这样看:对于 \(y=i\) ,与该折线有多少次相交,代表了它产生了多少次贡献。

对于这个东西我们如何处理呢?我们可以使用插入 \(dp\) ,将其看成一个个连续段,并计算新增的折线长度。

先将 \(A\) 从大到小排序并从大到小做 \(dp\),我们设 \(dp\) 数组 \(dp_{i,j,k}\) 分别表示处理到第 \(i\) 位、已有 \(j\) 个连续段、目前这线段长度为 \(k\) ,设 \(v=f_{i-1}-f_i\) ,那么有三种情况,分别讨论。

若新加进来的 \(f_i\) 作为一个新的连通块,考虑它可以放在哪里以及折线延长的长度,那么有:$ dp_{i,j,k}+=j \times dp_{i-1,j-1,k-2 \times (j-1) \times v}$ 。

若新加进来的 \(f_i\) 放到了一个连通块的一侧,那么连通块的数量是不变的,有:\(dp_{i,j,k}+=2 \times j \times dp_{i-1,j,k-2 \times j \times v}\)

若新加进来的 \(f_i\) 连接了两个连通块,那么连通块的数量要 \(-1\) ,有:\(dp_{i,j,k}+= j \times dp_{i-1,j+1,k-2 \times (j+1) \times v}\)

但是有一个很重要的一点:处于两端的点不会延伸折线,所以我们还要开两维 \(0/1\) 表示左端点/右端点是否放到排列中,相应的状态转移方程也需要分类,答案即为 \(dp_{n,1,L,1,1}\)

核心 \(dp\) 就做完了,细节注意要开滚动数组,时间复杂度 \(O(n^2m)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
long long n,m,a[1005],f[2][105][1005][2][2],s;
bool cmp(int q,int w){return q>w;} 
inline void dijah(long long &q,long long w)
{
	q=(q+w>=mod)?(q+w-mod):(q+w);
	return;
}
int main()
{
	int q,w,e;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	sort(a+1,a+n+1,cmp);
	f[1][1][0][0][0]=f[1][1][0][0][1]=f[1][1][0][1][0]=f[1][1][0][1][1]=1;
	for(register int i=1;i<n;i++)
	{
		e=i&1,q=e^1;
		for(register int k=1;k<=i;k++)
			for(register int j=0;j<=m;j++)
				for(register int q1=0;q1<2;q1++)
					for(register int q2=0;q2<2;q2++)
					{
						if(!f[e][k][j][q1][q2])continue;
						w=j+(2*k-q1-q2)*(a[i]-a[i+1]);
						if(w>m){f[e][k][j][q1][q2]=0;continue;}
						if(k>1)dijah(f[q][k+1][w][q1][q2],(k-1)*f[e][k][j][q1][q2]%mod);
						if(!q1)dijah(f[q][k+1][w][q1][q2],f[e][k][j][q1][q2]),dijah(f[q][k+1][w][1][q2],f[e][k][j][q1][q2]);
						if(!q2)dijah(f[q][k+1][w][q1][q2],f[e][k][j][q1][q2]),dijah(f[q][k+1][w][q1][1],f[e][k][j][q1][q2]);
						if(k>1)dijah(f[q][k][w][q1][q2],2*(k-1)*f[e][k][j][q1][q2]%mod);
						if(!q1)dijah(f[q][k][w][q1][q2],f[e][k][j][q1][q2]),dijah(f[q][k][w][1][q2],f[e][k][j][q1][q2]);
						if(!q2)dijah(f[q][k][w][q1][q2],f[e][k][j][q1][q2]),dijah(f[q][k][w][q1][1],f[e][k][j][q1][q2]);
						if(k>1)dijah(f[q][k-1][w][q1][q2],(k-1)*f[e][k][j][q1][q2]%mod);
						f[e][k][j][q1][q2]=0;
					}
	}
	for(int i=0;i<=m;i++)dijah(s,f[(n&1)][1][i][1][1]);
	printf("%lld",s);

  return 0;
}

【ARC118E】 Avoid Permutations

题目描述

对于一个排列 \(P\),定义 \(F(P)\) 如下:

对于一个 \((N+2)\times (N+2)\) 的网格图,行列标号为 \(0\sim N+1\),从 \((0,0)\) 走到 \((N+1,N+1)\) 在不经过 \((i,P_i)\) 情况下的方案数。

给定一个残缺的排列,对于其所有补全求函数之和,\(1 \le N \le 200\)

解题思路

我们先来考虑一个完整的排列我们该如何求和,有两种方法:\(n^2\) 遍历一遍整个图做一个统计路径个数的 \(dp\) , 同样 \(n^2\) 做一个带容斥的 \(dp\)

由于问题可能有缺项,直接 \(dp\) 显然是不现实的,我们考虑套一个容斥上去。

我们先转换贡献体,将贡献变成一条路径不会经过的排列个数之和,很明显,直接统计还是不好做,但我们套上一个容斥就好做了。

我们把不能经过的点叫做实点,因为是容斥,我们注意一个点:以前经过的实点对之后不会产生影响,那我们可以设计 \(dp\) 了。

\(dp_{i,j,0/1,0/1}\) 表示做到点 \((i,j)\) 、本行有没有实点、本列是否有实点,所有已经给出的实点都不经过,但是不是非常好转移,因为难以统计可以在那些地方放实点。

我们不妨再加一维,设 \(dp_{i,j,k,0/1,0/1}\) 表示做到点 \((i,j)\) 、已经经过了 \(k\) 个实点 、本行有没有实点、本列是否有实点,因为我们是容斥,没必要考虑是否还会经过其它实点的情况,当然,已给出的还是不经过。

转移不难,正常转移即可,设 \(m\) 为已给出点的个数,答案即为 \(\sum_{i=0}^{n-m} (n-m-i)! f_{n+1,n+1,i,0,0}\)

时间复杂度 \(O(n^3)\) ,常数可能略大。

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
int n,a[205],k,f[205][205][205][2][2];
long long p[1005],s=0;
bool v[205][205],v1[205],v2[205];
int main()
{
	scanf("%d",&n),p[0]=1;
	for(int i=1;i<=1000;i++)p[i]=p[i-1]*i%mod;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		if(a[i]!=-1)v[i][a[i]]=1,k++,v1[i]=1,v2[a[i]]=1;
	}
	f[0][0][0][1][1]=1;
	for(int i=0;i<=n+1;i++)
		for(int j=0;j<=n+1;j++)
			for(int u=0;u<=n;u++)
				for(int u1=0;u1<=1;u1++)
					for(int u2=0;u2<=1;u2++)
					{
						if(!v[i][j+1])
						{
							f[i][j+1][u][u1][v2[j+1]]=(f[i][j+1][u][u1][v2[j+1]]+f[i][j][u][u1][u2])%mod;
							if((!v2[j+1])&&j<n&&(u1==0))f[i][j+1][u+1][1][1]=(f[i][j+1][u+1][1][1]-f[i][j][u][u1][u2])%mod;
						}
						if(!v[i+1][j])
						{
							f[i+1][j][u][v1[i+1]][u2]=(f[i+1][j][u][v1[i+1]][u2]+f[i][j][u][u1][u2])%mod;
							if((!v1[i+1])&&i<n&&(u2==0))f[i+1][j][u+1][1][1]=(f[i+1][j][u+1][1][1]-f[i][j][u][u1][u2])%mod;
						}
					}
	for(int i=0;i<=n-k;i++)s=(s+p[n-k-i]*f[n+1][n+1][i][0][0])%mod;
	printf("%lld",(s+mod)%mod);


  return 0;
}

【Luogu P6047】 丝之割

题目描述

下面这部分题面只是为了帮助你理解题意,并没有详细的解释。更为严谨清晰的叙述见形式化题意。

多弦琴由两根支柱和连接两根支柱的 \(m\) 条弦组成。每根支柱上都均匀安放着 \(n\) 个固定点,第 \(i\) 条弦连接上方支柱的第 \(u_i\) 个固定点和下方支柱的第 \(v_i\) 个固定点。

为了摧毁多弦琴,你可以进行若干次切割操作。在一次切割操作中,你可以选择上方支柱的某一个固定点 \(u\) 和下方支柱的一个固定点 \(v\),所有被 \(u\)\(v\) 的连线从左到右穿过的弦都将被破坏。但同时,你需要付出 \(a_u \times b_v\) 的代价。

形式化题意:有 \(m\) 条弦,一条弦可以抽象为一个二元组 \((u,v)\),你可以进行任意次切割操作,一次切割操作你将选择两个下标 \(i\)\(j\) 满足 \(i,j \in [1,n]\),然后所有满足 \(u>i,v<j\) 的弦 \((u,v)\) 都将被破坏,同时你将付出 \(a_i \times b_j\) 的代价。求破坏所有弦的最小代价和,\(1 \le n,m \le 3 \times 10^5\)

解题思路

我们能发现一个东西,对于一条弦 \(i\) ,若存在 \(j\) \(u_j \le u_i,v_i \le v_j\) ,那么弦 \(i\) 是没必要记录的。

那么我们删去一些弦,那么剩余的弦按 \(u_i\) 排序后 \(v_i\) 一定是递增的。

据此我们就可以设计 \(dp\) 了,每次 \(dp\) 一段表示将一段的弦一次性割掉,时间复杂度是 \(O(n^2)\) 的。

很明显可以用斜率优化,因为有单调关系可优化到 \(O(n)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y;
}v1[1000005],v[1000005];
long long n,mm,m,a[1000005],b[1000005],d[1000005],l,r,f[1000005];
double T(long long x,long long y)
{
	if(a[v[x+1].x-1]!=a[v[y+1].x-1])return double(double(double(f[x])-double(f[y]))/double(double(a[v[y+1].x-1])-double(a[v[x+1].x-1])));
	else if(f[x]>f[y])return -1e9-5;
	else return 1e9+5;
}
bool cmp(datay q,datay w)
{
	if(q.x!=w.x)return q.x<w.x;
	else return q.y<w.y;
}
int main()
{
	scanf("%lld%lld",&n,&mm);
	a[0]=1e9+5;
	b[n+1]=1e9+5;
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		a[i]=min(a[i-1],a[i]);
	}
	for(int i=1;i<=n;i++)scanf("%lld",&b[i]);
	for(int i=n;i>=1;i--)b[i]=min(b[i+1],b[i]);
	for(int i=1;i<=mm;i++)
	{
		scanf("%lld%lld",&v1[i].x,&v1[i].y);
	}
	sort(v1+1,v1+mm+1,cmp);
	long long x=0;
	for(int i=1;i<=mm;i++)
	{
		if(v1[i].y>x)
		{
			v[++m]=v1[i];
			x=v1[i].y;
		}
	}
	d[++r]=0;
	l=1;
	for(int i=1;i<=m;i++)
	{
		while(r-l>=1&&T(d[l],d[l+1])<=b[v[i].y+1])l++;
		f[i]=f[d[l]]+a[v[d[l]+1].x-1]*b[v[i].y+1];
		while(r-l>=1&&T(d[r-1],d[r])>T(d[r],i))r--;
		d[++r]=i;
	}
	cout<<f[m];


  return 0;
}

【Luogu P3349】 小星星

题目描述

小 Y 是一个心灵手巧的女孩子,她喜欢手工制作一些小饰品。她有 \(n\) 颗小星星,用 \(m\) 条彩色的细线串了起来,每条细线连着两颗小星星。

有一天她发现,她的饰品被破坏了,很多细线都被拆掉了。这个饰品只剩下了 \(n-1\) 条细线,但通过这些细线,这颗小星星还是被串在一起,也就是这些小星星通过这些细线形成了树。小 Y 找到了这个饰品的设计图纸,她想知道现在饰品中的小星星对应着原来图纸上的哪些小星星。如果现在饰品中两颗小星星有细线相连,那么要求对应的小星星原来的图纸上也有细线相连。小 Y 想知道有多少种可能的对应方式。

只有你告诉了她正确的答案,她才会把小饰品做为礼物送给你呢,其中 \(1 \le n\le 17\)

解题思路

我们很容易想到一个树形 \(dp\)\(f_{i,j,k}\) 表示处理到第 \(i\) 个节点,把当前节点放到原 \(j\) 节点上,且被使用的原节点的方案为 \(k\)

状态转移方程很好推,时间复杂度是 \(O(n^23^n)\) 的。

瓶颈在于每次转移时的枚举子集,考虑优化。

枚举子集是无法避免的,考虑将 \(k\) 从状态中移出去,我们发现之所以要存 \(k\) ,是因为原节点不能有重复。

那我们可以用容斥,用不考虑重复的情况减去有重复的情况,但还是不好做。

我们这样理解:每个原节点都必须出现一遍,那我们可以这样做容斥:每次枚举一个子集 \(S\) 表示这次的原节点都必须在 \(S\) 内,然后根据 \(|S|\) 来判断前面要不要乘 \(-1\)

这就是一种二项式反演,每次枚举完 \(O(n^2)\) \(dp\) 求方案数即可。

时间复杂度 \(O(n^2 2^n)\) ,能过。

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,p,q,d[25];
long long s,f[25][25];
vector<int> a[25],a1[25][150005];
void dfs(int x,int y)
{
	long long h=0; 
	for(int i=1;i<=n;i++)
	{
		if((!((1<<(i-1))&q))||a[x].size()>d[i])f[x][i]=0;
		else f[x][i]=1;
	}
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs(a[x][i],x);
		for(int j=1;j<=n;j++)
		{
			if((!((1<<(j-1))&q))||a[x].size()>d[j])continue;
			h=0;
			for(int u=0;u<a1[j][q].size();u++)h+=f[a[x][i]][a1[j][q][u]];
			f[x][j]*=h;
		}
	}
	return;
}
int main()
{
	int x,y,w;
	scanf("%d%d",&n,&m),p=(1<<n);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y),q=(1<<(x-1)),w=(1<<(y-1)),d[x]++,d[y]++;
		for(int j=0;j<p;j++)
			if((q&j)&&(w&j))a1[x][j].push_back(y),a1[y][j].push_back(x); 
	}
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		a[x].push_back(y),a[y].push_back(x);
	}
	for(int i=0;i<p;i++)
	{
		w=n;
		for(int j=0;j<n;j++)
			if(!(i&(1<<j)))w--;
		q=i;
		dfs(1,0);
		for(int j=1;j<=n;j++)
			s+=f[1][j]*(((n-w)&1)?(-1):1);
	}
	printf("%lld",s);


  return 0;
}

【Luogu P10175】 Subtree Value

题目描述

给出一棵 \(n\) 个节点的树,每个点有点权 \(a_v\)。定义一棵树的一个子连通块为一个树中点的非空集合,满足这些点在树上形成一个连通块。定义子连通块 \(S\) 的权值为 \(\prod_{v\in S}(a_v+|S|)\)。求所有子连通块的权值之和对 \(U^V\) 取模, \(1 \le n \le 2000,1 \le U \le 10,1 \le V \le 6\)

解题思路

我们可以很快想出一个 \(O(n^3)\)\(dp\)\(f_{i,j,k}\) 表示处理到节点 \(i\) ,预计连通块大小为 \(j\) ,当前已构成了一个大小为 \(k\) 的连通块。

这个 \(dp\) 慢就慢在要同时记录两个连通块大小,考虑去掉其中一个。

我们之所以要记录预计连通块大小是因为每个点的价值要加上连通块大小,注意到答案要模的位 \(U^V\) ,其中 \(U,V\) 都不大。

我们考虑把 \(|S|\) 拆成 \(Ux+y\) 的形式,这样对于 \(a_v+|S|\) 我们就可以改写成 \(Ux+(a_v+y)\) ,这样我们就能把贡献式看成一个多项式。

由于 \(V\) 也很小,那么选择了大于等于个 \(Ux\) 是肯定的不会产生贡献的,我们只需要考虑当前值乘上了多少个 \(Ux\)

对于 \(|S|\) \(mod\) \(p1=u\)\(S\) ,它们的 \(a_v\) 加上的都是 \(u\) ,那么我们就可以把这些东西一起处理掉。

考虑这样设计 \(dp\)\(f_{i,j,k}\) 表示处理到第 \(i\) 个节点、连通块当前大小为 \(j\) 、当前项中有 \(k\)\(Ux\) 的总贡献,因为 \(Ux\) 固定,我们可以 \(dp\) 结束之后在处理它。

转移我们可以使用树形背包,时间复杂度为 \(O(n^2UV^2)\)

有个小技巧,转移时可以枚举完在一起取模,这样会快很多。

Code

#include<bits/stdc++.h>
using namespace std;
int n,siz[2005],k;
long long mod,p1,p2,v[2005],t[2005][6],s,f[2005][2005][6];
vector<int> a[2005];
void dfs(int x)
{
	f[x][1][1]=1;
	f[x][1][0]=(k+v[x])%mod;
	siz[x]=1;
	for(int i=0;i<a[x].size();i++)
	{
		dfs(a[x][i]);
		memset(t,0,sizeof(t));
		for(int j1=0;j1<=siz[x];j1++)
			for(int u1=0;u1<p2;u1++)
				for(int j2=0;j2<=siz[a[x][i]];j2++)
					for(int u2=0;u2<p2;u2++)
						if(u1+u2<p2)t[j1+j2][u1+u2]+=f[x][j1][u1]*f[a[x][i]][j2][u2];
		siz[x]+=siz[a[x][i]];
		for(int j=0;j<=siz[x];j++)
			for(int u=0;u<p2;u++)f[x][j][u]=(t[j][u]+f[x][j][u])%mod;
	}
	return;
}
int main()
{
	long long x;
	scanf("%d%lld%lld",&n,&p1,&p2),mod=1;
	for(int i=1;i<=p2;i++)mod*=p1;
	for(int i=2;i<=n;i++)scanf("%lld",&x),a[x].push_back(i);
	for(int i=1;i<=n;i++)scanf("%lld",&v[i]);
	for(int i=0;i<p1;i++)
	{
		memset(f,0,sizeof(f)),k=i;
		dfs(1);
		for(int j=1;j<=n;j++)
			for(int u=i;u<=n;u+=p1)
			{
				x=1;
				for(int q=0;q<p2;q++)
					s=(s+x*f[j][u][q])%mod,x*=(u/p1)*p1,x%=mod;
			}
	}
	printf("%lld",s);

  return 0;
}

【ARC184D】 Erase Balls 2D

题目描述

在二维平面上有 \(N\) 个编号从 \(1\)\(N\) 的球,第 \(i\) 个位于 \((X_i, Y_i)\)。其中,\(X = (X_1, X_2, \cdots, X_n)\) 以及 \(Y = (Y_1, Y_2, \cdots, Y_n)\) 分别是一个 \(1, 2, \cdots, n\) 的排列(译注:即横纵坐标分别两两不同)。

你可以执行任意次以下操作:

  • 从剩下的球中选择一个球,记为 \(k\)。对剩下的每个球 \(i\),若满足「\(X_i < X_k \)\(Y_i < Y_k\)」或「\(X_i > X_k\)\(Y_i > Y_k\)」(译注:即两球坐标间有二维偏序关系),将其移除。

求操作结束后,可能的剩下球的集合的数量,对 \(998244353\) 取模。

  • \(1 \le N \le 300\)

解题思路

统计剩下球的集合数量不好做,我们转换贡献体,考虑选择的球的集合 \(S\)

首先集合 \(S\) 内部不能存在两个数 \(x,y\) 使得点 \(x\) 与点 \(y\) 存在偏序关系,根据这一点我们可以想出来一个 \(O(n^2)\) 的找最长不下降子序列的 \(dp\)

但这个 \(dp\) 很明显会重复统计很多 \(S\),因为很多个选择的球的集合剩下的球的集合是完全一样的,我们考虑从剩下的球的集合构建一个对应关系使得与选择的球的集合一一对应。

我们把集合 \(S\) 中一个点称作必要的当且仅当删去该点剩余球的集合会变,那么我们考虑统计全为必要点的集合 \(S\) 个数,但进行 \(dp\) 转移时,我们发现由于后效性,我们不好统计哪些点为必要点。

换一种统计方式,我们考虑统计满足再多选一个数剩余球的集合就会变的集合 \(S\) ,即把非必要点都选上。

那这样转移就很好做了,我们只需要设 \(f_i\) 满足做到第 \(i\) 个球且选择第 \(i\) 个球的方案数,转移可以从 \(j \in [0,i-1]\) 转移过来,检验是否能转移只需找区间 \((i,j)\) 是否存在非必要点即可。

时间复杂度 \(O(n^3)\)

Code

#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
using namespace std;
const long long mod=998244353;
struct datay
{
	int x,y;
}a[305];
int n;
bool cmp1(datay q,datay w){return q.x<w.x;}
long long f[305];
int d[305];
int main()
{
	int q,w;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y);
	sort(a+1,a+n+1,cmp1);
	a[0].y=n+1,a[n+1].y=0,f[0]=1;
	for(int i=1;i<=n+1;i++)
		for(int j=0;j<i;j++)
		{
			if(a[j].y<a[i].y)continue;
			w=n+2,q=0;
			for(int u=j+1;u<i;u++)d[u]=0;
			for(int u=j+1;u<i;u++)
			{
				if(a[u].y<a[i].y||a[u].y>a[j].y)continue;
				if(w>a[u].y)d[u]=1;
				w=min(w,a[u].y);
			}
			w=-1;
			for(int u=i-1;u>j;u--)
			{
				if(a[u].y<a[i].y||a[u].y>a[j].y)continue;
				if(w<a[u].y&&d[u])q=1;
				w=max(w,a[u].y);
			}
			if(q==0)f[i]=(f[j]+f[i])%mod;
		}
	printf("%lld",f[n+1]);

  return 0;
}

数据结构

【Luogu P10856】 Xor-Forces

题目描述

给定一个长度为 \(n=2^k\) 的数组 \(a\),下标从 \(0\) 开始,维护 \(m\) 次操作:

  1. 操作一:给定 \(x\),设数列 \(a'\) 满足 \(a'_i=a_{i\oplus x}\),将 \(a\) 修改为 \(a'\)。其中 \(\oplus\) 表示按位异或运算。
  2. 操作二:给定 \(l,r\),查询 \(a\) 的下标在 \(l,r\) 之间的子数组有多少颜色段。不保证 \({l\le r}\),若 \({l > r}\),请自行交换 \({l,r}\)

其中,一个极长的所有数都相等的子数组称为一个颜色段。

部分测试点要求强制在线,\(0 \le k \le 18\)

解题思路

首先有一条极其重要的性质为数组长度为 \(2\) 的整数幂,因为这种题一般都用线段树,所以这是一条很重要的性质。

我们思考 \(a_{i \oplus x}\) 这个操作的性质,对于 \(x\) 的第 \(j\) 位,若其为 \(1\) ,那么 \(i\) 就会异或上 \(2^j\) ,也就是 \(a_i\)\(a_{i \oplus 2^j}\) 发生交换。

我们发现这个交换可以看成线段树上的某些块对换,所以我们可以线段树遍历时,若该位为 \(1\) ,那就换一个方向遍历。

那么就可以进行查询了,修改直接把直接异或之和存下来即可。

最后一个问题:怎么处理改变位置的操作对每个块内部相对位置的影响。

我们可以想到,不改变块内相对位置的位是不用理它的,所以对于一个第 \(i\) 层的块,若一共有 \(k\) 层,那么只用管 \(k-i\) 位。

由于层数越大块数越多,我们可以把会影响块内位置的存下来,即只存前 \(k-i\) 位的,很明显只用存 \(k2^k\) 个块 。

时间复杂度即为 \(O(k2^k)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	int lc,rc,v;
};
int qwe,n,k,m,v1[1000005],s;
vector<datay> t[2000005];
datay merge(datay x,datay y)
{
	datay h;
	h.lc=x.lc,h.rc=y.rc;
	h.v=(x.v+y.v-(x.rc==y.lc));
	return h;
}
void build(int x,int l,int r,int p)
{
	if(l==r)
	{
		datay h;
		h.lc=h.rc=v1[l],h.v=1;
		t[x].push_back(h);
		return;
	}
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1,z=p>>1; 
	build(lc,l,mid,z),build(rc,mid+1,r,z);
	for(int i=0;i<p;i++)
	{
		if(i&z)t[x].push_back(merge(t[rc][i^z],t[lc][i^z]));
		else t[x].push_back(merge(t[lc][i],t[rc][i]));
	}
	return;
}
datay query(int x,int l,int r,int ql,int qr,int p)
{
	if(ql<=l&&r<=qr)return t[x][(p-1)&s];
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1,z=p>>1;
	if(s&z)swap(lc,rc);
	if(qr<=mid)return query(lc,l,mid,ql,qr,z);
	if(ql>mid)return query(rc,mid+1,r,ql,qr,z);
	return merge(query(lc,l,mid,ql,qr,z),query(rc,mid+1,r,ql,qr,z));
}
int main()
{
	int lst=0,op,x,y;
	scanf("%d%d%d",&qwe,&k,&m);
	n=(1<<k);
	for(int i=0;i<n;i++)scanf("%d",&v1[i]);
	build(1,0,n-1,n);
	for(int i=1;i<=m;i++)
	{
		scanf("%d",&op);
		if(op==1)
		{
			scanf("%d",&x),x^=(lst*qwe);
			s^=x;
		}
		else
		{
			scanf("%d%d",&x,&y);
			x^=(lst*qwe),y^=(lst*qwe);
			if(x>y)swap(x,y); 
			lst=query(1,0,n-1,x,y,n).v;
			printf("%d\n",lst);
		}
	}
  return 0;
}

【Luogu P4587】 神秘数

题目描述

一个可重复数字集合 \(S\) 的神秘数定义为最小的不能被 \(S\) 的子集的和表示的正整数。例如 \(S=\{1,1,1,4,13\}\),有:\(1 = 1\)\(2 = 1+1\)\(3 = 1+1+1\)\(4 = 4\)\(5 = 4+1\)\(6 = 4+1+1\)\(7 = 4+1+1+1\)

\(8\) 无法表示为集合 \(S\) 的子集的和,故集合 \(S\) 的神秘数为 \(8\)

现给定长度为 \(n\)正整数序列 \(a\)\(m\) 次询问,每次询问包含两个参数 \(l,r\),你需要求出由 \(a_l,a_{l+1},\cdots,a_r\) 所组成的可重集合的神秘数,\(1\le n,m\le {10}^5\)\(\sum a\le {10}^9\)

解题思路

我们先来分析一个集合 \(S\) 怎样找到神秘数。

对于一个集合 \(S\) ,我们先找集合中是否存在 \(1\) ,若没有直接输出,否则 \(1\) 即为可以构成的,再来找 \(2\) ,若 \(2\) 存在,那么 \(\le 3\) 的数都是可以构成的,再来找 \(4\) ,若 \(4\) 存在,那么 \(\le 7\) 的数都是可以构成的,接着继续找下去 \(\cdots\)

我们发现从小到大遍历一个集合 \(S\) ,不断更新可以构成的区间 \([1,k]\) ,若新加进来的数 \(x \le k+1\) ,就说明了 \([k+1,k+x]\) 能在原来的基础上加上 \(x\) 来构成,加上即可,否则答案就为 \(k +1\)

我们开一棵值域线段树用来存储 \(S\) ,按上述流程进行查找,因为每次查找都会翻一倍,所以总共会有 \(logn\) 次查找,总时间复杂度为 \(O(log^2n)\)

现在是区间询问,我们只需要开一棵主席树即可,时间复杂度 \(O(nlog^2n)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	int lc,rc,v;
}f[10000005];
const int maxx=1e9;
int n,m,a[100005],num,v1[100005],root[100005];
int modify(int x,int l,int r,int k,int v)
{
	int h=++num;f[h]=f[x];
	if(l==r){f[h].v+=v;return h;}
	int mid=(l+r)>>1;
	if(k<=mid)f[h].lc=modify(f[x].lc,l,mid,k,v);
	else f[h].rc=modify(f[x].rc,mid+1,r,k,v);
	f[h].v=f[f[h].lc].v+f[f[h].rc].v;
	return h;
}
int query(int x,int l,int r,int ql,int qr)
{
	if(ql<=l&&r<=qr)return f[x].v;
	int mid=(l+r)>>1,h=0;
	if(ql<=mid&&f[x].lc)h+=query(f[x].lc,l,mid,ql,qr);
	if(qr>mid&&f[x].rc)h+=query(f[x].rc,mid+1,r,ql,qr);
	return h;
}
int solve(int l,int r)
{
	if(query(root[r],1,maxx,1,1)-query(root[l-1],1,maxx,1,1)==0)return 1;
	int x=1,s=1,y,pre=1;
	for(int i=1;i<=n;i++)
	{
		if(pre>s)break;
		y=query(root[r],1,maxx,pre,s)-query(root[l-1],1,maxx,pre,s);
		pre=s+1,s+=y;
	}
	return s;
}
int main()
{
	int x=0,y;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)root[i]=modify(root[i-1],1,maxx,a[i],a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		if(x>y)swap(x,y);
		printf("%d\n",solve(x,y));
	}

  return 0;
}

杂题

【Luogu P10991】 选段排序

题目描述

给定一个长度为 \(n\) 的序列 \(A_i\) 以及两个下标 \(p, q(p < q)\)。你可以选择任意一个区间 \([L, R]\) 并将序列的这个范围内的元素 \(A_L \sim A_R\) 从小到大排序。

求选择一个区间排序后 \(A_q − A_p\) 的值最大可以是多少,\(1 \le n \le 2 \times 10^5,1 \le V \le 10^6\)

解题思路

这题不好做贪心,我们来猜一下性质。

对于一个区间 \([l,r]\) ,满足 \(l<p\)\(r>q\) ,那么这个区间 \([l,r]\) 肯定是不优的,感性理解即可,证明考虑反证法,发现缩小区间一定会比原来好,矛盾得证。

那对于区间 \([l,r]\) ,若同时包含了 \(p,q\) ,那么 \(l=p\)\(r=q\)

对于 \(l>p\)\(r<q\) 的区间,我们发现他们的答案是肯定小于等于 \(l=p+1\)\(r=q-1\) 时的,而 \(l=p+1\)\(r=q-1\) 时答案明显时不会比 \(l=p\)\(r=q\) 时更优的,所以最大值所排的区间定有一端点为 \(p\)\(q\)

我们用个数据结构维护即可,可以用优先队列,这里用了线段树,时间复杂度 \(O(nlogn)\)

Code

#include<bits/stdc++.h>
using namespace std;
int n,k1,k2,a[200005],f[4000005],s,maxx;
void modify(int x,int l,int r,int k,int v)
{
	if(l==r){f[x]+=v;return;}
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(k<=mid)modify(lc,l,mid,k,v);
	else modify(rc,mid+1,r,k,v);
	f[x]=f[lc]+f[rc];
	return;
}
int query(int x,int l,int r,int k)
{
	if(l==r)return l;
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(f[lc]<k)return query(rc,mid+1,r,k-f[lc]);
	return query(lc,l,mid,k);
}
int main()
{
	scanf("%d%d%d",&n,&k1,&k2);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),maxx=max(maxx,a[i]);
	for(int i=k1;i<k2;i++)modify(1,1,maxx,a[i],1);
	for(int i=k2;i<=n;i++)
	{
		modify(1,1,maxx,a[i],1);
		s=max(s,query(1,1,maxx,k2-k1+1)-query(1,1,maxx,1));
	}
	memset(f,0,sizeof(f));
	for(int i=k1+1;i<=k2;i++)modify(1,1,maxx,a[i],1);
	for(int i=k1;i>=1;i--)
	{
		modify(1,1,maxx,a[i],1);
		s=max(s,query(1,1,maxx,k2-i+1)-query(1,1,maxx,k1-i+1));
	}
	printf("%d",s);
	
  return 0;
}
posted @ 2024-10-29 08:53  dijah  阅读(15)  评论(0编辑  收藏  举报