[作业题] #7 CF626G & CF627F & CF605E
Raffles
题目描述
点此看题
解法
首先考虑没有询问怎么做,考虑对第 \(i\) 个奖池增加一张彩票的贡献是(设现在的彩票数是 \(c_i
那么按照这个贡献排序之后贪心即可,十分简单。
那么修改怎么办呢?我自己的想法是使用调整法,修改之后最优方案可能会变化,那么我们减少某个奖池的彩票 \(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)否则甚至高斯消元都无法解决问题,那么我们从小到大来确定每个 \(E(i)\),一旦确定之后它的值就不会再改变了(因为自环的概率是 \(1\))
发现这个过程很像 \(\tt dijkstra\),我们先让 \(E(n)=0\),让后每次去找最小的 \(E(v)\),更新其他节点的 \(E(u)\) 即可。实现细节:分开维护等号右边的值和 \(\prod_{E(v)
如果你没有想到上面的方法怎么办?考虑随机一个 \(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