一类树上合并贪心问题 + Monster Hunter 问题偏序关系
树上一类贪心,都是父亲操作完儿子才能操作。都有一定贪心策略:
假设偏序关系有若干贪心:提出当前 最小值对应位置 \(x\),走完 \(fa_x\) 后一定走 \(x\)。
所以把 \(x\) 的信息合并到父亲上,丢掉新堆里,并且用并查集合并 \(x\to fa_x\),用堆维护这个贪心,直到缩成一个点即可。
只是一个简短的介绍,具体过程看下面例题。
后俩例题用到 Monster Hunter 模型,最后一题用单调栈合并。可以参考下面具体内容。
P4437
考虑到先序顺序进行图论建模。对于每一个 \(i\),将 \(a_i\) 向 \(i\) 连边。于是当图出现环时无解。有解则转化为一个以 \(0\) 为根的树。
设当前权值最小的节点为 \(x\),那么如果 \(x\) 之前的节点被染过色了,\(x\) 就一定要染色。不妨将 \(x\) 与之前的节点合并成一个块。
对于每一个块,我们需要判断如何调整先后顺序来使得贡献最小。先给一个贪心结论吧:根据平均数从小到大。
如何证明?
对于两个块 \(A\) 和 \(B\),我们设它们的大小分别为 \(n\) 和 \(m\)。
若将 \(A\) 排在 \(B\) 前进行操作,贡献即为 \(\sum\limits_{i=1}^{n} A_i \times i + \sum\limits_{i=1}^{m} B_i \times (i + n)\)。
若将 \(B\) 排在 \(A\) 前进行操作,贡献即为 \(\sum\limits_{i=1}^{m} B_i \times i + \sum\limits_{i=1}^{n} A_i \times (i + m)\)。
相减得出:\(m \sum\limits_{i=1}^{n} A_i - n \sum\limits_{i=1}^{m} B_i\)。
当该值 \(< 0\) 时,也就是如下时,\(A\) 排在 \(B\) 前进行操作贡献更小。否则反之:
\[\begin{aligned} m \sum\limits_{i=1}^{n} A_i &- n \sum\limits_{i=1}^{m} B_i < 0\\ m \sum\limits_{i=1}^{n} A_i &< n \sum\limits_{i=1}^{m} B_i\\ \frac{1}{n}\sum\limits_{i=1}^{n} A_i &< \frac{1}{m}\sum\limits_{i=1}^{m} B_i \end{aligned}\]易发现,这便是 \(A\) 和 \(B\) 两者的平均数。证毕。
使用并查集维护块的大小、总和,使用小根堆维护当前权值最小的节点即可。时间复杂度 \(\mathcal{O}(n \log n)\)。
$\bf{code}$
#include<bits/stdc++.h>
#define LL long long
#define fr(x) freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);
using namespace std;
const int N=5e5+5;
int n,a[N],fa[N],sz[N];LL ans,w[N];
struct node
{
int i,sz;LL x;
bool operator<(node X)const{return x*X.sz>X.x*sz;}
};
priority_queue<node>q;
inline void cl(){fill(sz,sz+1+n,1);iota(fa,fa+1+n,0);}
inline int getf(int x){return x==fa[x]?x:fa[x]=getf(fa[x]);}
inline void hb(int x,int y)
{
x=getf(x),y=getf(y);
if(x==y){cout<<"-1";exit(0);}fa[y]=x;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n;cl();
for(int i=1;i<=n;i++) cin>>a[i],hb(i,a[i]);cl();
for(int i=1;i<=n;i++) cin>>w[i],q.push({i,1,w[i]});
while(!q.empty())
{
auto [i,s,x]=q.top();q.pop();
if(s!=sz[i=getf(i)]) continue;
int f=getf(a[i]);fa[i]=f;
ans+=w[i]*sz[f];sz[f]+=sz[i],w[f]+=w[i];
if(f) q.push({f,sz[f],w[f]});
}
return cout<<ans,0;
}
qoj 9532
发现有一个很显然的二分答案做法,我们先二分答案 \(X\),然后把 \(i\) 点的权值变成 \(w_i - X\),就是检验是否存在以这个点为根的权值和 \(\geq 0\) 的连通块了。这个问题是有一个很显然的贪心的:\(f_i\) 表示以 \(i\) 为根的答案,那么就有 \(f_i = w_i - X + \sum\limits_{y \in \text{son}(i)} \max(f_y, 0)\),可以认为是它只选择那些 \(> 0\) 的儿子。
但是发现二分没有前途,所以我们考虑对于 \(X\) 进行扫描,直接从大到小,那么所有点的权值也都从 \(w_i - X\) 开始从小往大增大。
如果我们能在这个过程中动态维护所有的 \(f_i\),那么我们就只需要关注每一个 \(f_i\) 变成 \(0\) 的时刻的 \(X\) 即可。
而对于一个 \(f_i\),其变成 \(0\) 之后,随着 \(X\) 的增大,其必然一直保持 \(f_i > 0\),那么在其父亲的决策中就必然会选择这个点。那么我们就可以让其和其父亲所在的连通块合并。
那么我们需要合并树上的两个连通块,以及查询当前所有 \(< 0\) 的 \(f_i\) 中最早变成 \(0\) 的 \(f_i\)(也就是找最小的 \(\frac{f_i}{w_i}\))。发现可以直接使用并查集和可删堆分别处理,时间复杂度 \(O(n \log n)\)。
qoj 7588
考虑哪些怪兽会被优先打败。我们优先打败 \(a_i < b_i\) 的怪兽。
对于这些怪兽,优先打败 \(a_i\) 较小的。对于 \(a_i > b_i\) 的怪兽,优先打败 \(b_i\) 较大的。
也就是说,我们给怪兽之间规划了一个偏序关系。
- 考虑这个信息还是可以合并的,\((a_i,b_i)\bigoplus(a_j,b_j)=(a_k,b_k)\) 的意义是先打败 \(i\),再打败 \(j\),等价于打败 \(k\)。
\(a_k=a_i+\max(0,a_j-b_i),b_k=b_j+\max(0,b_i-a_j)\)。
加入没有树的限制,通过以上偏序关系可以确定一个最优的攻击怪兽的次序 \(p_1,p_2,\cdots,p_{n-1}\)。
此时直接用堆维护是不行的,因为一个位置还得考虑所有子树对它的贡献,不能直接拿它的值来比较。
考虑套用这个模型,每次拿出偏序关系最小的,合并到它父亲上更新父亲的值,然后把扫描到堆里原来父亲就跳过。具体看代码。
这样一个 “另类反向”贪心 就保证了每次都做最优决策。
「核桃 NOI 组周赛 71」WX-78 与遗迹
机器人 WX-78 正在清理一处遗迹,它的目标是击败所有的发条生物。
这些发条生物沿一条直线路径排列,从左到右编号为 \(1\) 到 \(n\)。
WX-78 若想经过发条生物 \(i\) 所在的位置,必须先击败它。击败发条生物 \(i\) 需要消耗 \(a_i\) 点血量,同时该生物会掉落齿轮,WX-78 可通过拾取这些齿轮恢复 \(b_i\) 点血量。
在任意时刻,如果 WX-78 的血量降至 \(0\) 或更低,它将当场倒下。
遗迹的入口可能位于 \(n - 1\) 个位置中的任意一个,第 \(i\) 个入口位于发条生物 \(i\) 与发条生物 \(i + 1\) 之间。WX-78 想知道,对于每一个可能的入口位置,它至少需要多少初始血量,才能清理完整个遗迹并始终保持存活。
- \(1 \le n \le 3 \times 10^5\),\(0 \le a_i, b_i \le 10^9\)。
Monster Hunter 问题 存在偏序关系!!!
考虑哪些发条骑士会被优先打败。我们优先打败 \(a_i < b_i\) 的发条骑士。
对于这些发条骑士,优先打败 \(a_i\) 较小的。对于 \(a_i > b_i\) 的发条骑士,优先打败 \(b_i\) 较大的。
也就是说,我们给发条骑士之间规划了一个偏序关系。
- 考虑这个信息还是可以合并的,\((a_i,b_i)\bigoplus(a_j,b_j)=(a_k,b_k)\) 的意义是先打败 \(i\),再打败 \(j\),等价于打败 \(k\)。
先胡一个很错的东西:
比如做到 \(i\),此时直接看左右哪个小,然后扩展。
这东西是错的。比如 \(i\) 前面很优,但是 \(i\) 不优,就会出问题。
比如对于左侧,先考虑 全局最小值。
对于全局最小的发条骑士 \(p\),那么当它被解锁时,一定优先打他。
然后你合并 \(p,p+1\),不断重复这个过程。这类似单调栈维护,最终 单调栈 内剩下一些 单调 的点。
右侧也是一样维护。
根据单调性,此时再进行左右归并一定是对的。
相当于要维护 \(\mathcal{O}(n)\) 次加入某个信息,\(\mathcal{O}(n)\) 次删除某个信息。
然后求按大小顺序合并这些信息最后得出的结果。
直接平衡树或者离线离散化线段树维护。
懒得写平衡树选择了后者,常数较大。
时间复杂度 \(\mathcal{O}(n \log n)\)。
$\mathbf{code}$
#include<bits/stdc++.h>
#define LL long long
#define P pair<LL,LL>
#define pt pair<T,int>
#define fi first
#define se second
#define fr(x) freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);
using namespace std;
const int N=3e5+5;
int n,m,t,p[N];LL ans[N];
struct T
{
LL x,y;
inline bool operator==(T A){return x==A.x&&y==A.y;}
inline bool operator<(T A){
bool o=x<y,p=A.x<A.y;if(o^p) return o>p;
return o?(P){x,y}<(P){A.x,A.y}:(P){y,x}>(P){A.y,A.x};
}
inline friend T operator+(T A,T B){
return {A.x+max(0ll,B.x-A.y),B.y+max(0ll,A.y-B.x)};
}
inline void operator+=(T B){*this=*this+B;}
}a[N],S[N],A[N<<3];
pt b[N<<1],ad[N];vector<pt>dl[N];
inline bool operator<(pt A,pt B){return A.fi==B.fi?A.se<B.se:A.fi<B.fi;}
inline int id(pt x){return lower_bound(b+1,b+1+m,x)-b;}
inline void init()
{
auto ins=[&](T x,int i){b[++m]={S[++t]=x,i};};
for(int i=1;i<n;i++)
{
T x=a[i];
while(t&&S[t]<x) dl[i].push_back({S[t],p[t]}),x+=S[t--];
ad[i]={x,i};ins(x,i);p[t]=i;
}t=0;
for(int i=n;i>1;i--)
{
T x=a[i];
while(t&&S[t]<x) x+=S[t--];ins(x,i+n);
}t=0;sort(b+1,b+1+m);
}
void upd(int p,bool o,int l=1,int r=m,int w=1)
{
if(l==r) return A[w]=o?b[l].fi:(T){0,0},void();
int mid=(l+r)>>1;
p<=mid?upd(p,o,l,mid,w<<1):upd(p,o,mid+1,r,w<<1|1);
A[w]=A[w<<1]+A[w<<1|1];
}
int main()
{
cin.tie(0)->sync_with_stdio(0);cout.tie(0);cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].y;init();
for(int i=1;i<n;i++)
{
for(pt &j:dl[i]) upd(j.se=id(j),0);
upd(ad[i].se=id(ad[i]),1);
}
for(int i=n;i>1;i--)
{
if(i^n) upd(ad[i].se,0);
for(auto [u,v]:dl[i]) upd(v,1);
T x=a[i];
while(t&&S[t]<x) upd(id({S[t],p[t]}),0),x+=S[t--];
S[++t]=x;p[t]=i+n;upd(id({x,i+n}),1);
ans[i-1]=A[1].x+1;
}
for(int i=1;i<n;i++) cout<<ans[i]<<" ";
return 0;
}

浙公网安备 33010602011771号