ZJOI 2015 题目选做
幻想乡 Wi-Fi 搭建计划
题目描述
解法
博主暂时不会证明这个关键的 \(\tt observaion\)
有一个关键的 \(\tt observation\) 是:考虑一种选取网络架设点的方案,一定存在一种划分方式,使得将景点按照 \(x\) 排序之后,架设点覆盖一段连续的景点(注意这个结论只适用上方和下方单独存在的情况)
所以可以设计 \(dp\),设 \(dp[i][j][k]\) 表示考虑前 \(i\) 个景点,前 \(j\) 个上方的架设点,前 \(k\) 个下方的架设点,当前正在覆盖的架设点是上方的第 \(j\) 个和下方的第 \(k\) 个的最小代价。那么转移考虑用 \(j/k\) 覆盖景点 \(i\),或者在上方 \(/\) 下方拓展一个新的架设点来覆盖景点 \(i\),这种转移需要枚举一下,时间复杂度 \(O(n^4)\)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 105;
#define int long long
const int inf = 1e18;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,R,k1,k2,k3,ans,dp[M][M][M];
struct node{int x,y,w;}a[M],b[M],c[M],d[M];
int sqr(int x) {return x*x;}
void upd(int &x,int y) {x=min(x,y);}
int dis(node A,node B)
{
return sqr(A.x-B.x)+sqr(A.y-B.y)<=R*R;
}
signed main()
{
n=read();m=read();R=read();
for(int i=1;i<=n;i++)
a[i].x=read(),a[i].y=read();
for(int i=1;i<=m;i++)
{
int x=read(),y=read(),w=read();
if(y<0) b[++k1]={x,y,w};
else c[++k2]={x,y,w};
}
for(int i=1;i<=n;i++)
{
bool f=0;
for(int j=1;j<=k1;j++) f|=dis(a[i],b[j]);
for(int j=1;j<=k2;j++) f|=dis(a[i],c[j]);
if(f) d[++k3]=a[i];
}
auto cmp = [&] (node A,node B) {return A.x<B.x;};
sort(b+1,b+1+k1,cmp);
sort(c+1,c+1+k2,cmp);
sort(d+1,d+1+k3,cmp);
memset(dp,0x3f,sizeof dp);
ans=inf;dp[0][0][0]=0;
for(int i=1;i<=k3;i++) for(int j=0;j<=k1;j++)
for(int k=0;k<=k2;k++) if(dp[i-1][j][k]<inf)
{
int t=dp[i-1][j][k];
if((j && dis(d[i],b[j])) ||
(k && dis(d[i],c[k]))) upd(dp[i][j][k],t);
for(int l=j+1;l<=k1;l++) if(dis(d[i],b[l]))
upd(dp[i][l][k],t+b[l].w);
for(int l=k+1;l<=k2;l++) if(dis(d[i],c[l]))
upd(dp[i][j][l],t+c[l].w);
}
for(int i=0;i<=k1;i++) for(int j=0;j<=k2;j++)
upd(ans,dp[k3][i][j]);
printf("%lld\n%lld\n",k3,ans);
}
地震后的幻想乡
题目描述
解法
概率和期望题一定要注意初始的转化啊,一般都有一个比较简洁的形式的。
充分利用题目中的提示,首先我们考虑暴力,可以暴力枚举边的大小关系,然后用 \(\tt kruskal\) 计算出至少要加入前 \(k\) 条边才能够联通,那么答案的期望就是 \(\frac{k}{m+1}\),每种大小关系是等概率出现的,所以枚举全排列即可。
暴力启发我们转化问题,我们可以求出,在所有边中选取 \(k\) 条边加入之后恰好联通的概率(这里就不要再纠结于排列了)。而这可以转化成,选取 \(k-1\) 条边加入之后不连通的概率,减去选取 \(k\) 条边加入之后不连通的概率。
这变成了一个较为简洁的图连通性问题,设 \(g(s,i)/f(s,i)\) 分别表示对于点集 \(s\),在他的导出子图中选择 \(i\) 条边,连通 \(/\) 不连通的方案数,首先有关系式,设 \(d_s\) 为 \(s\) 导出子图的边数:
考虑 \(f(s,i)\) 的转移,其实就是枚举子集 \(t\),满足 \(t\) 是一个连通块并且 \(t\) 和 \(s-t\) 之间没有边,为了避免算重我们需要强制一个特定的点 \(k\) 在 \(t\) 中(转移的本质就是正难则反):
转移完成了,考虑最后的答案是:
时间复杂度 \(O(3^n\cdot m^2)\),但是我真的感觉这题没什么难点就是做不来。
总结
概率期望类型的题目中,一些前缀\(/\)差分类型的转化往往能够简化问题。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1<<10;
#define db double
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,a[50],b[50],d[M];
db ans,g[M][50],f[M][50],C[50][50];
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
a[i]=read()-1,b[i]=read()-1;
for(int i=0;i<=m;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
for(int s=0;s<(1<<n);s++)
for(int i=1;i<=m;i++)
d[s]+=((s>>a[i]&1)&&(s>>b[i]&1));
for(int s=0;s<(1<<n);s++) for(int i=0;i<=d[s];i++)
{
int k=s&(-s);
for(int t=(s-1)&s;t;t=(t-1)&s) if(t&k)
for(int j=0;j<=min(i,d[t]);j++)
f[s][i]+=g[t][j]*C[d[s^t]][i-j];
g[s][i]=C[d[s]][i]-f[s][i];
}
for(int i=0;i<=m;i++)
ans+=f[(1<<n)-1][i]/C[m][i];
ans=ans/(m+1);
printf("%.6f\n",ans);
}