巴蜀模拟赛10.30 总结

爆了...

T1

考虑树形背包。

显然,最优策略一定是在图上找一个节点数为K的子图拿出来,然后答案是这个导出子图每条边的权值乘2减去一个直径。

这个东西不好做,但是我们可以树形dp。

\(f(i,j,0/1/2)\)为第i个点在这个联通块内时,这个选择了的连通块的大小为j,i及其子树内部无直径/有直径的一端/有完整的直径 的最小答案。

有直径的一端即这一根直径是从i这个点出发到其它点。这个时候两根这样的半段直径就可以拼成一根完整的直径了。

所以我们有转移:

\(f(i,j,0)\)是整个子树内都没直径,直接做树形背包即可。即把j-1个点分配给所有的子树。

\(f(i,j,1)\)是有一根半段直径。这半段直径肯定从某一个子树中来,枚举这一个子树v是什么,\(dis(u,v)\)产生的代价即为一倍而非两倍。

\(f(i,j,2)\)也差不多,不过我们要注意有可能这个直径直接从某一个子树内继承,即\(f(v,k,2)\)转移。

当然也可以枚举两个半直径,即由 \(f(u,j,1)+f(v,k,1)\) 转移过来。(虽然看起来复杂度很高,但是树形背包每一次是前面的所有子树和当前一个子树进行合并,所以复杂度仍然是n平方)。

还学到了真正的n方树形背包。每一次枚举只枚举到子树大小这么多,这样每一个点对都只会被统计到一次。

直接看代码。

#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;
inline char gc()
{
	static char buf[1<<16],*S,*T;
	if(S==T)
	{
		T=(S=buf)+fread(buf,1,1<<16,stdin);
		if(S==T)exit(0);
	}
	return *(S++);
}
#define getchar gc
inline int r(){int s=0,k=1;char c=getchar();while(!isdigit(c)){if(c=='-')k=-1;c=getchar();}while(isdigit(c)){s=s*10+c-'0';c=getchar();}return s*k;}
int ans,head[1000001],cnt,f[3005][3005][3],n,t,g[3005][3],size[1000005];
struct node
{
	int to,dis,next;
}a[1000001];
void add_edge(int from,int to,int dis)
{
	a[++cnt].to=to;
	a[cnt].dis=dis;
	a[cnt].next=head[from];
	head[from]=cnt;
}
void dfs(int u,int fa)
{
	size[u]=1;
//	cout<<"dfs"<<u<<endl;
	for(int i=head[u];i;i=a[i].next)
	{
		int v=a[i].to;
		if(v==fa)continue;
		dfs(v,u);
		
		for(int j=0;j<=t;j++)
		for(int k=0;k<=2;k++)
		g[j][k]=1e9;
		
		for(int j=1;j<=size[u];j++)//当前子树分配了j个点 
		{
			for(int k=1;k<=size[v];k++)//v分配了k个点 
			{
				g[j+k][0]=min(g[j+k][0],f[u][j][0]+f[v][k][0]+a[i].dis*2);//没有长链:直接分配 
				g[j+k][1]=min(g[j+k][1],min(f[u][j][1]+f[v][k][0]+a[i].dis*2,f[u][j][0]+f[v][k][1]+a[i].dis));//一条长链:前面选 or 此处选
				g[j+k][2]=min(g[j+k][2],f[u][j][2]+f[v][k][0]+a[i].dis*2);//两条长链:前面选 or 集成子树的 or 当前拼接
				g[j+k][2]=min(g[j+k][2],f[u][j][0]+f[v][k][2]+a[i].dis*2);
				g[j+k][2]=min(g[j+k][2],f[u][j][1]+f[v][k][1]+a[i].dis);
			}
		}
		
		size[u]+=size[v];
		for(int j=0;j<=t;j++)
		for(int k=0;k<=2;k++)
		f[u][j][k]=min(f[u][j][k],g[j][k]);
	}

	ans=min(ans,f[u][t][2]);
}
int main()
{
	n=r();t=r();int x,y,z;
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++)
	for(int j=0;j<=2;j++)f[i][1][j]=0;
	ans=1e9;
	for(int i=1;i<n;i++)
	{
		x=r();y=r();z=r();
		add_edge(x,y,z);
		add_edge(y,x,z);
	}
	dfs(1,0);
	cout<<ans;
}

T2

显然答案上界是\(\sum b_i\),此时每一次操作都只操作一个点,是独立的。

我们考虑合并两次操作,答案就会减1。

我们需要尽量多的合并操作。这个合并一定是相邻的两个点进行合并。直接转化为父亲和儿子之前的问题。

显然尽量地和儿子合并后再和父亲合并一定不会更差。因为父亲的合并次数可能可以被其它位置消耗,但是它的儿子却不能了。

首先钦定一个根节点,设\(f(i)\)表示第i个点尽量地和儿子合并后还能够和父亲合并多少次。

那么这个合并方法就相当于我现在有\(b_i\)次机会进行合并,每一次可以合并最多\(p_i\)次。

然后每一次合并后某些机会可以合并的最多次数会减少。注意如果某一个儿子的可合并次数很多,也不能让它超过一整层。显然每一次我们都选择可以合并的次数最多那些机会去合并儿子。

这个计算就很麻烦。考虑简化。

首先拿出那些儿子可以合并次数超过了\(b_i\)的,我肯定每一次合并都会选择到它们。每一个这种节点相当于让所有机会的合并次数都减少1,把这些拿出来单独计算。

剩下的所有儿子在选择的时候都不可能一次直接选完一整层,所以我们可以直接乘法计算了。

void dfs(int u,int fa)
{
	vector<int>ve;
	for(int i=head[u];i;i=a[i].next)
	{
		int v=a[i].to;
		if(v==fa)continue;
		dfs(v,u);
		if(f[v])ve.push_back(f[v]);
	}
	int sum=0;
	for(int i=0;i<(int)ve.size();i++)
	{
		int x=ve[i];
		if(x>=b[u])p[u]--,ans-=b[u];
		else sum+=x;
	}
	if(sum>=p[u]*b[u])f[u]=0,ans-=p[u]*b[u];
	else f[u]=min(b[u],p[u]*b[u]-sum),ans-=sum;
//	cout<<"why:"<<u<<' '<<f[u]<<endl;
}

T3

直接dp会发现转移可能会转移到自己,即互相miss的情况。

考虑把这种情况给踢出去,让每一轮都能够造成一次伤害。

设A为Alice先手的时候给Bob造成伤害先手又回到Alice的概率,1-A即为Alice先手的时候Bob给Alice造成伤害先手又回到Alice的概率。这里Alice造成伤害后显然要回到自己Bob有一次反击的机会,这个反击要算到最后统计答案里面。

\[A=\sum_{i=1}^{+\inf}(i-p)^{i-1}(1-q)^{i-1}p=p\sum^{+\inf}_{i=1}[(1-p)(1-q)]^{i-1}=\frac{p}{p+q-pq} \]

这里需要用等比数列求和

\[\lim_{x\to +\inf}[(1-p)(1-q)]^x=0 \]

因为\((1-q)(1-p)<1\)

然后最后的概率直接用组合数计算,即为

\[\sum_{i=m}^{i-1}C_{i-1}^{m-1}A^m(1-A)^{i-m}\sum_{j=0}^{n-(i-m)-1}C^j_{m-1}q^i(1-q)^{m-1-j} \]

后面这个sum可以用前缀和预处理算出来。

复杂度为O(n+m)


T4

暴力dp有10分。

暴力李超树是60左右

正解是分治+可持久化李超树

跟我没关系

posted @ 2021-10-31 22:07  lei_yu  阅读(50)  评论(0编辑  收藏  举报