#20 CF303E & CF343E & CF360E

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;
}
posted @ 2022-05-25 09:28  C202044zxy  阅读(376)  评论(6编辑  收藏  举报