巴蜀模拟赛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有一次反击的机会,这个反击要算到最后统计答案里面。
这里需要用等比数列求和
因为\((1-q)(1-p)<1\)
然后最后的概率直接用组合数计算,即为
后面这个sum可以用前缀和预处理算出来。
复杂度为O(n+m)
T4
暴力dp有10分。
暴力李超树是60左右
正解是分治+可持久化李超树
跟我没关系
本文来自博客园,作者:lei_yu,转载请注明原文链接:https://www.cnblogs.com/lytql/p/15491146.html