[作业题] #7 CF626G & CF627F & CF605E


Raffles

题目描述

点此看题

解法

首先考虑没有询问怎么做,考虑对第 \(i\) 个奖池增加一张彩票的贡献是(设现在的彩票数是 \(c_i):

\[p_i(\frac{c_i+1}{c_i+1+l_i}-\frac{c_i}{c_i+l_i})=\frac{p_i\cdot c_i}{(c_i+l_i)(c_i+l_i+1)} \]

那么按照这个贡献排序之后贪心即可,十分简单。

那么修改怎么办呢?我自己的想法是使用调整法,修改之后最优方案可能会变化,那么我们减少某个奖池的彩票 \(1\),增加某个奖池的彩票 \(1\),用 \(\tt set\) 即可维护这个操作。

问题是操作次数,感性上操作次数不会很多,实际上最多发生一次调整。这里简单证明一下,我们考虑 \(l_i\) 增大的情况,对于同时选取增量的 \(x\)\(x+1\),贡献分别是:

\[\frac{p_c\cdot l_i}{(l_i+x)(l_i+x+1)},\frac{p_c\cdot l_i}{(l_i+x+1)(l_i+x_i+2)} \]

那么调整之后贡献分别是:

\[\frac{p_c\cdot (l_i+1)}{(l_i+x+1)(l_i+x+2)},\frac{p_c\cdot (l_i+1)}{(l_i+x+2)(l_i+x+3)} \]

我们观察调整之后 \(x\) 贡献大于调整之前 \(x+1\) 的贡献,既然调整之前 \(x+1\) 都被选取了,说明调整之后 \(x\) 一定会被选取,再讨论几种类似的情况即可证明这个结论。

时间复杂度 \(O(n\log n)\),有点卡精度,要用 \(\tt longdouble\)\(\tt eps\)

#include 
#include 
#include  
#include 
using namespace std;
const int M = 200005;
#define db long double
const db inf = 1e18;
const db eps = 1e-10;
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,p[M],l[M],c[M];db ans;
db f(int x,int c)
{
	if(c==-1) return inf;
	if(c>=l[x]) return 0L;
	return 1.0L*p[x]*l[x]/(c+1+l[x])/(c+l[x]); 
}
db g(int x)
{
	int t=min(l[x],c[x]);
	return 1.0L*p[x]*t/(t+l[x]);
}
db Abs(db x) {return x>0?x:-x;}
struct node
{
	db o;int x,y;
	node(int X=0,int Y=0) : x(X) , y(Y) {o=f(x,y);}
	bool operator < (const node &b) const
		{return Abs(o-b.o)>eps?o s,e;
void add()
{
	auto it=--s.end();ans+=it->o;int x=it->x;
	e.erase(node(x,c[x]-1));e.insert(*it);
	s.erase(it);s.insert(node(x,++c[x]));
}
void rem()
{
	auto it=e.begin();ans-=it->o;int x=it->x;
	s.erase(node(x,c[x]));s.insert(*it);
	e.erase(it);e.insert(node(x,(--c[x])-1));
}
signed main()
{
	n=read();m=read();k=read();
	for(int i=1;i<=n;i++) p[i]=read();
	for(int i=1;i<=n;i++) l[i]=read();
	for(int i=1;i<=n;i++)
		s.insert(node(i,0)),e.insert(node(i,-1));
	while(m--) add();
	while(k--)
	{
		int op=read(),x=read();
		s.erase(node(x,c[x]));e.erase(node(x,c[x]-1));
		ans-=g(x);l[x]+=(op==1)?1:-1;
		s.insert(node(x,c[x]));e.insert(node(x,c[x]-1));
		ans+=g(x);
		while((--s.end())->o>(e.begin()->o)+eps) rem(),add();
		printf("%.10Lf\n",ans);
	}
}

Island Puzzle

题目描述

点此看题

解法

话说这题关键方法差不多自己想清楚了,有一些细节和代码实现抄了 \(\tt xht\) 的。

首先考虑对操作的一些观察:首先操作具有可逆性,这说明调整法是可用的。此外由于 \(0\) 只有一个,我们可以把操作看成 \(0\) 的路径,正向\(/\)反向经过一个点的操作效果会抵消(经过环是同向所以具有不同的操作效果)

考虑树的情况,根据结论我们直接把 \(0\) 从初始位置 \(s\) 移动到目标位置 \(t\) 即可,可以先把它判掉。

再考虑如果给你一个环,怎么把初始状态变化到目标状态?首先我们把 \(0\) 的位置去掉,然后把剩下的点接成环,我们发现如果 \(0\) 逆时针走一圈回到原点,那么操作效果是剩下的点顺时针轮换一圈,所以求出圈数之后暴力轮换即可。

那么如何在原树找到这条环边呢?把 \(0\) 移动到 \(t\) 之后,我们考虑所有 \(a[i]\not=b[i]\) 的点 \(i\) 构成集合 \(S\),根据环的操作效果,\(S\) 和最低点的父亲 \(p\) 必然构成环,所以我们判据就是:\(p\) 是唯一的,并且 \(S\cup\{p\}\) 是一条路径。

然后我们先从 \(t\) 走到最低点 \(p\),然后用环的轮换方法处理,最后再从 \(p\) 走回 \(t\),需要判断可以轮换成目标权值。

接下来的问题就是最小步数了,首先轮换有顺时针和逆时针两种方法,都算出来取最小值即可。但是我们的步数可能不是最小的,因为我们的两部分路径可能还有重复的部分是可以省去的:

那么最后的代价表达式是(设 \(sz\) 为环的大小,\(c\) 表示轮换次数):

\[dis(s,u_0)+(c-1)\cdot sz+1+dis(u_1,t) \]

注意算代价时要区分顺逆时针,环应该是以 \(u_0\) 为起点排列的,这样逆时针移动才有顺时针轮换的效果。

#include 
#include 
#include  
#include 
#include 
using namespace std;
const int M = 200005;
#define ll long long
#define pb push_back
#define mio {puts("-1");exit(0);}
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,s,t,lu,ld,a[M],b[M],v[M],d[M];
vector A,B,h,o,g[M];
bool dfs1(int u,int fa)//find the path s->t
{
	o.pb(u);
	if(u==t) return 1;
	for(int v:g[u]) if(v^fa)
		if(dfs1(v,u)) return 1;
	o.pop_back();
	return 0;
}
void dfs2(int u,int fa,int d)//tag all the difference
{
	if(a[u]!=b[u])
	{
		h.pb(u);
		//maintain the shallow father
		if(d==ld+1 && fa!=lu) mio
		if(d u;
	ld=n;dfs2(t,0,0);h.pb(lu);
	for(int x:h) v[x]=1;
	for(int x:h) for(auto y:g[x])
		if(v[y]) d[y]++;
	for(auto x:h)
	{
		if(d[x]==1) u.pb(x);
		else if(d[x]!=2) mio
	}
	if(u.size()!=2) mio
	if(u[0]>u[1]) swap(u[0],u[1]);
	dfs3(u[0],0);
	if(!calc()) mio
	ll ans=get(u[0],u[1]);
	reverse(A.begin(),A.end());
	reverse(B.begin(),B.end());
	ans=min(ans,get(u[1],u[0]));
	printf("%d %d %lld\n",u[0],u[1],ans);
}

Intergalaxy Trips

题目描述

点此看题

解法

\(E(u)\) 表示从 \(u\) 走到 \(n\) 的最小天数,然后有是个人就会写的转移:

\[(1-\prod_{E(v)

稍微解释一下就是 \(E(x)\(x\) 都不开放,\(v\) 才会有贡献,如果 \(E(v)\(v\) 都不开放,那么就只能走 \(E(u)\) 了(相当于走 \(u\) 的自环),但是这个转移的前提是知道 \(E(i)\) 的大小关系,否则甚至高斯消元都无法解决问题,那么我们从小到大来确定每个 \(E(i)\),一旦确定之后它的值就不会再改变了(因为自环的概率是 \(1\)

发现这个过程很像 \(\tt dijkstra\),我们先让 \(E(n)=0\),让后每次去找最小的 \(E(v)\),更新其他节点的 \(E(u)\) 即可。实现细节:分开维护等号右边的值和 \(\prod_{E(v) 会更好计算一些。时间复杂度 \(O(n^2)\)


如果你没有想到上面的方法怎么办?考虑随机一个 \(E\) 的大小关系序列,那么我们知道这个序列就可以 \(O(n^2)\) 转移了,并且如果大小关系序列不正确那么答案只会变大。

可以用随机求出的 \(E\) 的大小关系当成下一次的大小关系序列,一直这样循环到超时即可。虽然我不是很理解这种做法的正确性,但是在允许精度误差的本题上有比较优异的表现。

总结

转移顺序还是一如既往的重要,如果转移的顺序是值,并且值大的不会影响值小的。那么可以用类似 \(\tt dijkstra\) 的方法,每次取出最小值并且固定,考虑它对其他值的影响即可。

#include 
const int M = 1005;
#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,vis[M];db g[M][M],E[M],p[M];
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			g[i][j]=read()/100.0;
	if(n==1) {puts("0");return 0;}
	for(int i=1;i