Random Ranking
题目描述
解法
首先考虑一个简单的问题,假设 \(k\) 个人的得分在 \([l,r]\) 中随机选取,那么某个人获得 \(1\sim k\) 排名的概率都是 \(\frac{1}{k}\)
那么我们考虑对于离散化后的每一个小段 \([p_i,p_{i+1})\),我们把一些人分配到小段的左边,把一些人分配到小段的右边,把 \(x\) 和一些人分配到小段中。那么计算出分配的概率,就可以套用上面的模型,得到 \(x\) 对应排名的概率。
那么问题转化成了计算这东西:\(f[i][a][b]\) 表示考虑前 \(i\) 个不是 \(x\) 的人,小段左边的人有 \(a\) 个,小段右边的人有 \(b\) 个,剩下的人都在小段中的概率。一个人的概率可以通过讨论线段关系简单计算,那么这就变成一个背包问题了。
时间复杂度 \(O(n^5)\),好像不是很能过的样子。但考虑排除某个点,又是加入的问题显然可以用分治优化,对于每个段批量处理,加入左半边的点递归到右半边,加入右半边的点递归到左半边,时间复杂度 \(O(n^4\log n)\)
总结
很多实数概率题的技巧通常是,先解决一个能用概率简单计算的子问题,然后把原问题化归到这个简单问题上(比如 地震后的幻想乡)
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 165;
#define db double
const db eps = 1e-8;
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,v,l[M],r[M],p[M],k[10];
db a[M][M],f[10][M][M],g[M][M],s[M][M][M];
db A[M][M],B[M][M],C[M][M];
db ins(int i,int x)
{
if(l[i]>x) return 0;
if(r[i]<x) return 1;
return 1.0*(x-l[i])/(r[i]-l[i]);
}
void add(int d,int x)
{
for(int i=0;i<=k[d];i++)
for(int j=0;i+j<=k[d];j++)
{
if(f[d][i][j]<eps) continue;
g[i+1][j]+=f[d][i][j]*A[x][v];
g[i][j+1]+=f[d][i][j]*B[x][v];
g[i][j]+=f[d][i][j]*C[x][v];
}
k[d]++;
for(int i=0;i<=k[d];i++)
for(int j=0;i+j<=k[d];j++)
f[d][i][j]=g[i][j],g[i][j]=0;
}
void cdq(int d,int l,int r)
{
if(l==r)
{
for(int i=0;i<=k[d];i++)
for(int j=0;i+j<=k[d];j++)
s[l][i][j]+=f[d][i][j]*B[l][v];
return ;
}
int mid=(l+r)>>1;
k[d+1]=k[d];
for(int i=0;i<=k[d];i++)
for(int j=0;i+j<=k[d];j++)
f[d+1][i][j]=f[d][i][j];
for(int i=l;i<=mid;i++) add(d+1,i);
cdq(d+1,mid+1,r);
k[d+1]=k[d];
for(int i=0;i<=k[d];i++)
for(int j=0;i+j<=k[d];j++)
f[d+1][i][j]=f[d][i][j];
for(int i=mid+1;i<=r;i++) add(d+1,i);
cdq(d+1,l,mid);
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
l[i]=read();r[i]=read();
p[++m]=l[i];p[++m]=r[i];
}
sort(p+1,p+1+m);
m=unique(p+1,p+1+m)-p-1;
for(int i=1;i<=n;i++)c++
for(int j=1;j<m;j++)
{
db x=ins(i,p[j]),y=ins(i,p[j+1]);
A[i][j]=x;
B[i][j]=y-x;
C[i][j]=1-y;
}
f[1][0][0]=1.0;k[1]=0;
for(v=1;v<m;v++) cdq(1,1,n);
for(int x=1;x<=n;x++) for(int i=0;i<n;i++)
for(int j=0;i+j<n;j++) for(int k=1;k<=j+1;k++)
a[x][i+k]+=s[x][i][j]/(j+1);
for(int i=1;i<=n;i++,puts(""))
for(int j=1;j<=n;j++)
printf("%.8f ",a[i][j]);
}
Pumping Stations
题目描述
解法
还是第一次用到最小割树这个知识点,能用得上去也是很不错了。
首先我们建出原图的最小割树,那么问题变成了寻找一个排列,使得相邻两点路径上最小值之和最大。
我们猜测答案上界是每条边恰好贡献一次。证明可以考虑调整法,我们取出最小的边 \((u,v)\),设 \(u\) 子树内的两个点 \((u_0,u_1)\),\(v\) 子树的两个点 \((v_0,v_1)\),如果按照 \(u_0,v_0,u_1,v_1\) 的顺序是没有 \(u_0,u_1,v_0,v_1\) 更优的,所以两个子树的点要挨在一起,那么这条边只会贡献一次。
所以可以递归地构造答案,每次找到最小的边,然后分成两棵子树递归下去即可。
#include <cstdio>
#include <queue>
using namespace std;
const int M = 2005;
const int inf = 0x3f3f3f3f;
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,k,tot=1,S,T,f[M],cur[M],d[M];
int ans,t,a[M],b[M],c[M],w[M],vis[M];
struct edge{int v,c,next;}e[M<<1];
struct node{int v,id;};vector<node> g[M];
//part I : network flow
void add(int u,int v,int c)
{
e[++tot]=edge{v,c,f[u]},f[u]=tot;
e[++tot]=edge{u,0,f[v]},f[v]=tot;
}
int bfs()
{
queue<int> q;
for(int i=1;i<=n;i++) d[i]=0;
q.push(S);d[S]=1;
while(!q.empty())
{
int u=q.front();q.pop();
if(u==T) return 1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(e[i].c>0 && !d[v])
d[v]=d[u]+1,q.push(v);
}
}
return 0;
}
int dfs(int u,int ept)
{
if(u==T) return ept;
int flow=0,tmp=0;
for(int &i=cur[u];i;i=e[i].next)
{
int v=e[i].v;
if(e[i].c>0 && d[v]==d[u]+1)
{
tmp=dfs(v,min(ept,e[i].c));
if(!tmp) continue;
flow+=tmp;ept-=tmp;
e[i].c-=tmp;e[i^1].c+=tmp;
if(!ept) break;
}
}
return flow;
}
int dinic(int A,int B)
{
for(int i=2;i<=tot;i+=2)
e[i].c+=e[i^1].c,e[i^1].c=0;
S=A;T=B;int r=0;
while(bfs())
{
for(int i=1;i<=n;i++) cur[i]=f[i];
r+=dfs(S,inf);
}
return r;
}
//part II : build the mincut-tree
void cdq(int l,int r)
{
if(l==r) return ;
w[++k]=dinic(a[l],a[l+1]);
g[a[l]].push_back({a[l+1],k});
g[a[l+1]].push_back({a[l],k});
int tl=0,tr=0;
for(int i=l;i<=r;i++)
{
if(d[a[i]]) b[++tl]=a[i];
else c[++tr]=a[i];
}
for(int i=1;i<=tl;i++) a[l+i-1]=b[i];
for(int i=1;i<=tr;i++) a[l+tl+i-1]=c[i];
cdq(l,l+tl-1);cdq(l+tl,r);
}
//part III : dfs to construct
void find(int u,int fa,int &mn)
{
for(auto x:g[u]) if(x.v^fa && !vis[x.id])
{
find(x.v,u,mn);
if(w[x.id]<w[mn])
mn=x.id,S=u,T=x.v;
}
}
void dfs(int u)
{
int x=0;find(u,0,x);
if(x==0) {a[++t]=u;return ;}
vis[x]=1;ans+=w[x];
int A=S,B=T;dfs(A);dfs(B);
}
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),c=read();
add(u,v,c);add(v,u,c);
}
for(int i=1;i<=n;i++) a[i]=i;
cdq(1,n);w[0]=inf;dfs(1);
printf("%d\n",ans);
for(int i=1;i<=n;i++)
printf("%d ",a[i]);
puts("");
}
Levko and Game
题目描述
解法
关键的 \(\tt observation\) 是:一条边只可能调整到 \(l_i\) 或 \(r_i\),证明考虑调整一条边:
- 如果没有任何人经过这条边,那么无影响。
- 如果 \(A\) 经过了这条边,\(B\) 没有经过这条边,那么肯定是尽可能调小,直到调到 \(l_i\)
- 如果 \(A\) 没有经过这条边,\(B\) 经过了这条边,那么肯定是尽可能调大,直到调到 \(r_i\)
- 如果 \(A,B\) 都经过了这条边,那么无影响。
并且通过这个观察我们可以得到大致的想法:对于 \(A\) 有利的边,我们取 \(l_i\),对于 \(B\) 有利的边,我们取 \(r_i\)
首先考虑判定 \(A\) 是否能赢,一开始我们把边都设置成 \(r_i\),如果跑出来的最短路满足 \(d_1(x)<d_2(x)\),那么我们把这条边改成 \(l_i\),这是基于关键结论:修改边权之后 \(\forall x,d_1(x)<d_2(x)\) 的关系不会改变:
考虑修改的边是 \((u,v)\),那么一定满足 \(d_1(u)<d_2(u)\),使用反证法,假设此时出现了某个 \(x\) 由 \(d_1(x)<d_2(x)\) 变成了 \(d_1(x)\geq d_2(x)\)
由于此时 \(d_2(x)\) 变小了,那么 \((u,v)\) 一定在 \(s_2\) 到 \(x\) 的最短路上,所以有 \(d_2(x)=d_2(u)+dis(u,x)\),但是我们又知道 \(d_1(x)\leq d_1(u)+dis(u,x)\),由于 \(d_1(u)<d_2(u)\),说明 \(d_1(x)<d_2(x)\),矛盾。
考虑判断平局,就是在满足 \(d_1(x)\leq d_2(x)\) 的时候修改,本质上是一样的。
由于只会修改 \(k\) 次,而每次都需要重新跑 \(\tt dijkstra\),所以时间复杂度 \(O(kn\log n)\)
总结
在给定区间确定权值的问题,可能有经典结论是权值只会在端点处取得(其他的例子一时想不起来了)
可以把本题和 Cow and Exercise 对比来理解,虽然都是都是涉及修改边权最短路的问题。后者是要最大化最短路,所以用费用流来划分路径类;但是本题只需要战胜对手即可,所以在分析时侧重两个起点到某点距离的大小关系及其变化。想清楚这点可以帮助你确定大方向,知道你要向什么地方去找性质。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int M = 20005;
#define int long long
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,k,s1,s2,t,tot,d1[M],d2[M],f[M];
int a[M],b[M],c[M],nw[M];
struct edge{int v,c,next;}e[M];
struct node
{
int u,c;
bool operator < (const node &b) const
{return c>b.c;}
};
void add(int u,int v,int c)
{
e[++tot]=edge{v,c,f[u]},f[u]=tot;
}
void dijk(int s,int *d)
{
priority_queue<node> q;
for(int i=1;i<=n;i++) d[i]=1e18;
q.push({s,0});d[s]=0;
while(!q.empty())
{
int u=q.top().u,w=q.top().c;q.pop();
if(d[u]<w) continue;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v,c=e[i].c;
if(d[v]>d[u]+c)
{
d[v]=d[u]+c;
q.push({v,d[v]});
}
}
}
}
int solve(int d)
{
int fl=0;
do
{
fl=0;
dijk(s1,d1);
dijk(s2,d2);
for(int i=1;i<=k;i++)
if(nw[i]!=c[i] && d1[a[i]]<d2[a[i]]+d)
{
nw[i]=c[i];
add(a[i],b[i],c[i]);
fl=1;
}
}while(fl);
return d1[t]<d2[t]+d;
}
signed main()
{
n=read();m=read();k=read();
s1=read();s2=read();t=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),c=read();
add(u,v,c);
}
for(int i=1;i<=k;i++)
{
int u=read(),v=read(),l=read(),r=read();
add(u,v,r);a[i]=u;b[i]=v;c[i]=l;nw[i]=r;
}
if(solve(0))
{
puts("WIN");
for(int i=1;i<=k;i++)
printf("%lld ",nw[i]);
puts("");
return 0;
}
if(solve(1))
{
puts("DRAW");
for(int i=1;i<=k;i++)
printf("%lld ",nw[i]);
puts("");
return 0;
}
puts("LOSE");
return 0;
}