8.19 训练总结

前言

太久没写总结了,今天来写几道题。

A. Phryctoria

题面

简单 DP,但是改初始化和求答案改了两天。

很容易想到最后的字符串必然是一段子串、一个 * 的形式,也就是 \(S\) 中的部分子串会被转为 *。

因此我们可以想到设个 DP,你问我怎么想到的,我只能说因为我想不到别的做法。

设 \(f_{i,j,0/1}\) 表示考虑到 \(S\) 的第 \(i\) 位,\(T\) 的第 \(j\) 位,最后以 * 或字符串结尾的答案。你发现这个东西没法转移,因为显然可能在前面时还不成立,但是在第 \(i\) 位时正好就成立了,所以我们改一下 DP 状态:设 \(f_{i,j,0/1}\) 表示考虑到 \(S\) 的第 \(i\) 位,\(T\) 的第 \(j\) 位,最后以 * 或字符串结尾,且当前压缩后的字符串能与 \(T\) 的前 \(j\) 位匹配,但不能和任意一个前缀匹配。

此时我们可以写出转移:

  1. 最后插入一个 *:此时我们可以把一个任何长度的子串压缩成 *,所以转移就是从 \(f_{i,j}\) 转移到 \(f_{i+k,j}\)。
  2. 最后插入一段子串:此时我们必须保证这段子串同时出现在了 \(T\) 从 \(j\) 开始的后缀里,否则我们就找到了一种答案。

假设第二种情况时我们找到的在 \(T\) 中最早出现子串的位置是 \(p\),那么显然可以从 \(f_{i,j}\) 转移到 \(f_{i+k,p+k}\)。

但是有一种特殊情况:如果最终压缩后的字符串是一个 * 带一串字符串(或者反过来),此时 DP 是算不出来的,需要手动添加答案。

代码:

const int N=506;
int n,m,la[26],mn[N],d[N][N],f[N][N][2];
string s,t;
signed main()
{
	n=read(),m=read();
	cin>>s>>t;
	s=" "+s,t=" "+t;
	for(int i=n;i>=1;i--)
	{
		for(int j=m;j>=1;j--)
		{
			if(s[i]==t[j])
			{
				d[i][j]=d[i+1][j+1]+1;
			}
		}
	}
	memset(f,0x3f,sizeof(f));
	f[0][0][0]=0;
    int ans=n;
    ans=min(ans,d[1][1]+1+min(1ll,n-d[1][1]-1));//特殊情况
    int mx=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<m;j++)
        {
            if(j+d[i][j]-1==m&&i+d[i][j]-1==n)
            {
                mx=max(mx,d[i][j]);
            }
        }
    }
    ans=min(ans,mx+1+min(1ll,n-mx-1));
	for(int i=0;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			for(int k=0;k<=n-i;k++)
			{
				f[i+k][j][1]=min(f[i+k][j][1],f[i][j][0]+1);
			}
			for(int k=0;k<N;k++)
			{
				mn[k]=1e18;
			}
			for(int k=j+1;k<=m;k++)
			{
				mn[d[i+1][k]]=min(mn[d[i+1][k]],k);
			}
			for(int k=N-2;k>=1;k--)
			{
				mn[k]=min(mn[k],mn[k+1]);
			}
			for(int k=1;k<=n-i;k++)
			{
				if(mn[k]>=(int)(1e18))
				{
                    ans=min(ans,min(f[i][j][1],f[i][j][0])+k+min(1ll,n-i-k));//计算答案
					continue;
				}
				int p=mn[k];
				if(p+k-1>m+1)
				{
					continue;
				}
                f[i+k][p+k-1][0]=min(f[i+k][p+k-1][0],min(f[i][j][1],f[i][j][0])+k);
			}
		}
	}
	write(ans);
	return 0;
}

B. Mountain hike

题面

还行吧,简单题,就是想正确性的时候想错了。

首先显然整个图是一个 DAG,因此我们可以想到设一个看起来很对的 DP:设 \(f_{i,j}\) 表示第一条路径走到了 \(i\) 点,第二条路径走到了 \(j\) 点,此时的最短路径总和是多少。

但是你会发现你并不知道下一步是否被经过过没。此时我们可以利用拓扑序:显然在拓扑序上每个点只会一直往后跳。因此我们考虑每次让靠前的那个点跳。

根据这个条件我们可以得出:靠前的点不可能在靠后的点前面连续跳两次。所以每次判断我们只需要判断靠后的点下一步是否会跳到靠前的点的位置上(因为靠前的点的上一步必然在靠后的点的后面),如果不会显然可以更新。

最后就是有点细节,调一下就行了。

代码:

const int N=1e3+6;
int n,m,st1,ed1,st2,ed2,cnt,t[N],d[N],f[N][N];
vector<pair<int,int>>v[N];
signed main()
{
    n=read(),m=read();
	for(int i=1,x,y,s;i<=m;i++)
	{
		x=read(),y=read(),s=read();
		v[x].push_back(make_pair(y,s));
		d[y]++;
	}
	queue<int>q;
	for(int i=1;i<=n;i++)
	{
		if(!d[i])
		{
			q.push(i);
		}
	}
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		t[++cnt]=u;
		for(auto i:v[u])
		{
			d[i.first]--;
			if(!d[i.first])
			{
				q.push(i.first);
			}
		}
	}
	st1=read(),ed1=read(),st2=read(),ed2=read();
	memset(f,0x3f,sizeof(f));
	f[st1][st2]=0;
	for(int i=1;i<=cnt;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			int x=t[i],y=t[j];
			if(i<j||y==ed2)
			{
				for(auto k:v[x])
				{
					if(k.first!=y)
					{
						f[k.first][y]=min(f[k.first][y],f[x][y]+k.second);
					}
				}
			}
			if(i>j||x==ed1)
			{
				for(auto k:v[y])
				{
					if(k.first!=x)
					{
						f[x][k.first]=min(f[x][k.first],f[x][y]+k.second);
					}
				}
			}
		}
	}
	if(f[ed1][ed2]>=(int)(1e18))
	{
		write("NIE");
	}
	else
	{
		write(f[ed1][ed2]);
	}
	putchar('\n');
}

C. 位运算

题面

奇奇妙妙计数题。

首先你会发现只要至少有一个 \(1\),那你最终就必定能形成一个 \(1\)。

而 \(0\) 和 \(1\) 之间的与、或运算可以看作删除其中一个数,于是问题就被我们抽象成了:

对于每个 \(a_i=1\) 的 \(i\),你可以删除 \([1,i]\) 中的数直到只剩下 \(a_i\),再删除 \([i,n]\) 中的数直到只剩下 \(a_i\)。

显然每次前面删除的操作方案数是 \((2i-1)!!\),,后面删除的操作方案数是 \((2(n-i)-1)!!\),因为前后操作顺序不定,所以还要乘 \(\binom{n-1}{i-1}\)。

代码:

const int N=1e6+6,mod=998244353;
int n,a[N],fac[N],ffac[N<<1],inv[N];
int qpow(int x,int y)
{
	int z=1;
	while(y)
	{
		if(y&1)
		{
			z=z*x%mod;
		}
		x=x*x%mod;
		y>>=1;
	}
	return z;
}
int C(int x,int y)
{
	return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
signed main()
{
    n=read();
	read(a+1,a+n+1);
	ffac[1]=1;
	for(int i=3;i<=n*2;i+=2)
	{
		ffac[i]=ffac[i-2]*i%mod;
	}
	fac[0]=1;
	for(int i=1;i<=n;i++)
	{
		fac[i]=fac[i-1]*i%mod;
	}
	inv[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i>=0;i--)
	{
		inv[i]=inv[i+1]*(i+1)%mod;
	}
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		if(a[i])
		{
			ans=(ans+(2*(i-1)-1<0?1:ffac[2*(i-1)-1])*(2*(n-i)-1<0?1:ffac[2*(n-i)-1])%mod*C(n-1,i-1)%mod)%mod;
		}
	}
	write(ans);
	putchar('\n');
	return 0;
}

D. 最近公共祖先

题面

我们先考虑一个暴力做法:枚举点 \(u\) 作为当前钦定的 LCA,那么显然任一满足 \(\operatorname{LCA}(x,y)=u\) 的点对 \((x,y)\) 必须在 \(u\) 的两棵不同的子树里。

所以我们可以想到依次枚举子树,然后用当前子树与前面的子树匹配。这样可以做到 \(O(n)\) 的复杂度。

但是现在要求我们在两棵树中同时成立,我们考虑在 \(T\) 树中延用上面的方法,那么 \(S\) 树中的怎么办?我们可以在 \(S\) 树上把前面遍历过的节点标记出来,然后用 \(S\) 树计算同时匹配的个数。

怎么计算这个数呢?我们枚举当前 \(T\) 树内的这棵子树上的所有点,然后在 \(S\) 上找到这个点所在的子树,那么用 \(S\) 树中 \(u\) 子树的贡献值减去这个点所在的子树的贡献值就行了。这个显然可以用 dfs 序加 BIT 解决。

最后一个问题:如何在正确的时间复杂度内枚举子树上的点?这个可以用 DSU on tree,于是时间复杂度:\(O(n\log^2n)\)。

代码:

const int N=2e5+6;
int n;
namespace Tree1
{
	struct BIT{
		int len,tr[N];
		#define lowbit(x) (x&-x)
		void init(int x)
		{
			len=x;
			for(int i=1;i<=x;i++)
			{
				tr[i]=0;
			}
		}
		void update(int x,int y)
		{
			for(int i=x;i<=len;i+=lowbit(i))
			{
				tr[i]+=y;
			}
		}
		int query(int x)
		{
			int ans=0;
			for(int i=x;i;i-=lowbit(i))
			{
				ans+=tr[i];
			}
			return ans;
		}
	}tr;
	vector<int>v[N],son[N];
	int cnt,L[N],R[N];
	void dfs(int x,int fa)
	{
		L[x]=++cnt;
		for(auto i:v[x])
		{
			if(i==fa)
			{
				continue;
			}
			dfs(i,x);
			son[x].push_back(i);
		}
		R[x]=cnt;
	}
	void init()
	{
		tr.init(n);
		cnt=0;
		for(int i=1;i<=n;i++)
		{
			v[i].clear();
			son[i].clear();
		}
		for(int i=1,x,y;i<n;i++)
		{
			x=read(),y=read();
			v[x].push_back(y);
			v[y].push_back(x);
		}
		dfs(1,0);
	}
	void update(int x,int y)
	{
		tr.update(L[x],y);
	}
	int find(int x,int y)
	{
		int l=0,r=son[x].size()-1,ans=0;
		while(l<=r)
		{
			int mid=l+r>>1;
			if(L[son[x][mid]]<=L[y])
			{
				ans=mid;
				l=mid+1;
			}
			else
			{
				r=mid-1;
			}
		}
		return son[x][ans];
	}
	int query(int x,int y)
	{
		if(L[x]<=L[y]&&R[x]>=R[y])
		{
			y=find(x,y);
			return tr.query(R[x])-tr.query(L[x]-1)-(tr.query(R[y])-tr.query(L[y]-1));
		}
		return 0;
	}
	int query1(int x)
	{
		return tr.query(R[x])-tr.query(L[x]-1);
	}
}
using Tree1::update;
using Tree1::query;
using Tree1::query1;
namespace Tree2
{
	int cnt,ans,siz[N],son[N],L[N],R[N],dfn[N];
	vector<int>v[N];
	void dfs1(int x,int fa)
	{
		siz[x]=1;
		L[x]=++cnt;
		dfn[cnt]=x;
		son[x]=0;
		for(auto i:v[x])
		{
			if(i==fa)
			{
				continue;
			}
			dfs1(i,x);
			siz[x]+=siz[i];
			if(siz[i]>siz[son[x]])
			{
				son[x]=i;
			}
		}
		R[x]=cnt;
	}
	void dfs(int x,int fa)
	{
		for(auto i:v[x])
		{
			if(i==fa||i==son[x])
			{
				continue;
			}
			dfs(i,x);
			for(int j=L[i];j<=R[i];j++)
			{
				update(dfn[j],-1);
			}
		}
		if(son[x])
		{
			dfs(son[x],x);
		}
		for(auto i:v[x])
		{
			if(i==fa||i==son[x])
			{
				continue;
			}
			for(int j=L[i];j<=R[i];j++)
			{
				ans+=query(x,dfn[j]);
			}
			for(int j=L[i];j<=R[i];j++)
			{
				update(dfn[j],1);
			}
		}
		ans+=query1(x);
		update(x,1);
	}
	void init()
	{
		cnt=ans=0;
		for(int i=1;i<=n;i++)
		{
			v[i].clear();
		}
		for(int i=1,x,y;i<n;i++)
		{
			x=read(),y=read();
			v[x].push_back(y);
			v[y].push_back(x);
		}
		dfs1(1,0);
		dfs(1,0);
		write(ans);
		putchar('\n');
	}
}
signed main()
{
	int T=read();
	while(T--)
	{
		n=read();
		Tree1::init();
		Tree2::init();
	}
	return 0;
}
posted @ 2026-08-20 20:31  cwxcplh  阅读(7)  评论(0)    收藏  举报