省选集训
AGC018C
好像挺典的,贪心 trick。
三维限制,首先容易想到全分配给一号,然后往出取,变成从 \(n\) 个物品中取 \(v_2\) 个给二号,\(v_3\) 个给三号,贡献变成差值。
然后就变成 CF730I。
考虑贪心,临项交换法,假如放进一号的贡献为 \(a\),放进二号的贡献为 \(b\)。
假如两个物品 \(i,j\) 都放进去,且 \(i\) 放一号 \(j\) 放二号比 \(i\) 放二号 \(j\) 放一号优,那么有:
交换得到:
因此我们按 \(a-b\) 排序,那么存在一个分界点,使前缀中选了所有放进二号的,后缀中选了所有放进一号的。
优先队列可以预处理前缀中选 \(v\) 个物品的最大值,同理后缀也可以。统计答案即可。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1e5+5;
int n,v[3],c[N];
LL ans,a[N][3],f[N];
priority_queue<LL> q;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
for(int i=0;i<3;i++) scanf("%d",&v[i]),n+=v[i];
for(int i=1;i<=n;i++) for(int j=0;j<3;j++) scanf("%lld",&a[i][j]);
for(int i=1;i<=n;i++)
{
ans+=a[i][2]; c[i]=i;
a[i][1]-=a[i][2]; a[i][0]-=a[i][2];
}
sort(c+1,c+1+n,[&](const int &x,const int &y){return a[x][1]-a[x][0]<a[y][1]-a[y][0];});
LL now=0;
for(int i=1;i<=n;i++)
{
if(i<=v[0]) q.push(-a[c[i]][0]),now+=a[c[i]][0];
else
{
LL tmp=a[c[i]][0]+q.top();
if(tmp>0) q.pop(),q.push(-a[c[i]][0]),now+=tmp;
}
if(i>=v[0]) f[i]=now;
}
while(!q.empty()) q.pop();
now=0; LL res=-1e18;
for(int i=n;i>=1;i--)
{
if(n-i+1<=v[1]) q.push(-a[c[i]][1]),now+=a[c[i]][1];
else
{
LL tmp=a[c[i]][1]+q.top();
if(tmp>0) q.pop(),q.push(-a[c[i]][1]),now+=tmp;
}
if(n-i+1>=v[1]&&i-1>=v[0]) res=max(res,f[i-1]+now);
}
printf("%lld\n",ans+res);
return 0;
}
AGC032E
还是贪心。人类智慧?
从小到大排序,所有匹配对可以分为两类:\(\lt m\) 的和 \(\ge m\) 的。
对于同一类,显然存在包含关系时最优(最大的和最小的、次大的和次小的...)。
对于不同类的,可以证明并列关系最优(大分讨)。
所以一定有一个分界点,使前缀中只有第一类,后缀中只有第二类。
显然,分界点越靠右越优,二分可解。
场上真的需要证明贪心吗?
code
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int n,a[N],m;
inline bool check(int mid)
{
for(int l=mid+1,r=n;l<r;l++,r--)
{
if(a[l]+a[r]<m) return 0;
}
return 1;
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&n,&m);n<<=1;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
sort(a+1,a+1+n);
int l=0,r=n,res=1;
while(l<=r)
{
int mid=l+r>>1;
if(check(mid)) r=mid-1,res=mid;
else l=mid+1;
}
if(res&1) res++;
int ans=0;
for(int i=1,j=res;i<j;i++,j--)
ans=max(ans,(a[i]+a[j]));
for(int i=res+1,j=n;i<j;i++,j--)
ans=max(ans,(a[i]+a[j])-m);
printf("%d\n",ans);
return 0;
}
[JSOI2007] 建筑抢修
朴素贪心。
按截止时间排序。
先能选就选,选不了考虑能不能替换之前的。
如果能使总花费时间变小的话一定不劣,所以开堆记一下之前的最大值。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1.5e5+5;
int n;
struct A {int x,y;} a[N];
priority_queue<int> q;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&a[i].x,&a[i].y);
sort(a+1,a+1+n,[&](const A &x,const A &y){return x.y<y.y;});
LL sum=0; int ans=0;
for(int i=1;i<=n;i++)
{
int tmp=q.top();
if(sum+a[i].x<=a[i].y) sum+=a[i].x,q.push(a[i].x),ans++;
else if(tmp>a[i].x) q.pop(),sum+=a[i].x-tmp,q.push(a[i].x);
}
printf("%d\n",ans);
return 0;
}
最短路
\(n\) 年以前的题。
由于最短路唯一,想到建最短路树(如果不唯一不一定是树)。
断掉树边,加一条连向子树外的非树边,新的贡献就是 \(d_v+w\),发现对于每一条边 \(v_i=d_u+d_v+w\) 是一定的,对点 \(u\) 的贡献可以由 \(v_i-d_u\) 得到。
按 \(v_i\) 从小到大加入边,中间可以用并查集维护已更新过得点,复杂度近似 \(O(n)\)。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5,M = 2e5+5;
#define LL long long
int n,m;
int head[N],tot;
struct E {int u,v,w;} e[M<<1],ed[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
LL d[N],ans[N];
bool vs[N];
int dep[N],f[N],fa[N];
inline int find(int x) {return fa[x]==x?(x):(fa[x]=find(fa[x]));}
inline void dj(int s)
{
priority_queue<pair<LL,int> > q;
memset(ans,0x3f,sizeof(ans));
memset(d,0x3f,sizeof(d));
d[s]=0; q.push({0,s});
while(!q.empty())
{
int u=q.top().second; q.pop();
if(vs[u]) continue;
vs[u]=1;
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(!vs[v]&&d[v]>d[u]+e[i].w)
{
d[v]=d[u]+e[i].w; dep[v]=dep[u]+1; f[v]=u;
q.push({-d[v],v});
}
}
}
}
inline void work(int x,int y,LL w)
{
while(x!=y)
{
if(dep[x]<dep[y]) swap(x,y);
ans[x]=min(ans[x],w-d[x]);
fa[find(x)]=find(f[x]);
x=find(f[x]);
}
}
int main()
{
freopen("path.in","r",stdin);
freopen("path.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x,y,z; scanf("%d%d%d",&x,&y,&z);
add(x,y,z); add(y,x,z); ed[i]={x,y,z};
}
dj(1);
sort(ed+1,ed+1+m,[&](const E &x,const E &y){return d[x.u]+d[x.v]+x.w<d[y.u]+d[y.v]+y.w;});
for(int i=1;i<=m;i++)
{
int u=ed[i].u,v=ed[i].v,w=ed[i].w;
if(d[u]==d[v]+w||d[v]==d[u]+w) continue;
work(u,v,d[u]+d[v]+w);
}
for(int i=2;i<=n;i++) printf("%lld\n",ans[i]>=1e11?(-1):(ans[i]));
return 0;
}
购物
感觉自己当年能想到还是挺牛的。
假如选所有物品,\(s=\sum a_i\),\(k\) 的范围显然是 \([\lceil{\frac{s}{2}}\rceil,s]\)
将所有物品从小到大排序,考虑删去最小的物品后 \(s\) 仍大于等于 \(\lceil{\frac{s}{2}}\rceil\),
因此重复上述操作能得到一个连续的区间,[\lceil{\frac{a_{max}}{2}}\rceil,s]。
这是一开始选择所有物品,要想扩大区间发现只和最大值有关,每次删去最大值即可,复杂度 \(O(n)\)。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1e5+5;
int n,a[N];
LL sum,ans;
int main()
{
freopen("buy.in","r",stdin);
freopen("buy.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),sum+=a[i];
sort(a+1,a+1+n);
LL l,r=sum,L=r+1,R;
for(int i=n;i>=1;i--)
{
l=ceil((1.0*a[i]/2));
R=min(r,L-1); L=max(l,1ll);
ans+=R-L+1;
r-=a[i];
}
printf("%lld\n",ans);
return 0;
}
TEST_100
trick,值域折叠。
处理绝对值问题常用方法。注意到绝对值实际上可以转化为两点间距离。
而距离对称点的两点完全等价。
假设一开始有 \(x\),进行一次操作就是 \(|x-a_i|\),也就是 \(x\) 到 \(a_i\) 的距离,考虑对原点进行操作。
原来 \(a_i\) 的位置 \(|x-a_i|=0\),因此让距离 \(x\) 为 \(a_i\) 的点做新的原点,只考虑操作后原点在有效值域上(不在就不用操作了)。
然后根据对称点完全等价的性质将较小的一半对折过去,用并查集维护即可。
实际上和 回收 Bot 是一样的。
CF702F
平衡树好题。
学习 插入-标记-回收 维护函数复合。
显然这是一个分段函数复合问题。
按上述方法,我们将查询作为节点插入数据结构中。然后通过打标记的方式进行修改。
本题显然比较好做直接做。
FHQ 维护子树减,子树加即可。发现子树减之后需要进行平衡树有交合并。
可以按类似归并的方法,每次找出两棵树中最小的一段,然后依次加入新树。
复杂度为 \(O(n\log^2 n)\),证明用到势能函数,详见 平衡树有交合并复杂度证明。
另一种解释是复杂度正确性是基于本题性质:
假如要减去的数是 \(c\),那么两棵树可以分裂成 \([0,c),[c,2c),[2c,\infty)\),有交的只有中间一段。
并且对于中间这段,每次操作会使其整体除二,那么最多进行 \(log\) 次操作。
实现时直接 \(log\) 查最小值比维护子树最小值要快,问?
code
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int n,m,ans[N];
struct A {int a,b;} a[N];
namespace FHQ
{
struct D {int x,id,ans;} va[N];
int rt,tot,pr[N],sz[N],tag[N],son[N][2],ta[N];
inline void pushup(int k) {sz[k]=sz[son[k][0]]+sz[son[k][1]]+1;}
inline void add(int k,int x) {va[k].x+=x; tag[k]+=x;}
inline void ad(int k,int x) {va[k].ans+=x; ta[k]+=x;}
inline void pushdown(int k)
{
if(tag[k])
{
int lz=tag[k]; tag[k]=0;
add(son[k][0],lz); add(son[k][1],lz);
}
if(ta[k])
{
int lz=ta[k]; ta[k]=0;
ad(son[k][0],lz); ad(son[k][1],lz);
}
}
inline int merge(int x,int y)
{
if(!x||!y) return x|y;
if(pr[x]<=pr[y]) return pushdown(x),son[x][1]=merge(son[x][1],y),pushup(x),x;
else return pushdown(y),son[y][0]=merge(x,son[y][0]),pushup(y),y;
}
inline void split(int rt,int &x,int &y,int k)
{
if(!rt) return x=y=0,void(0);
pushdown(rt);
if(va[rt].x<=k) x=rt,split(son[x][1],son[x][1],y,k);
else y=rt,split(son[y][0],x,son[y][0],k);
pushup(rt);
}
inline int kth(int rt,int k)
{
pushdown(rt);
if(sz[son[rt][0]]>=k) return kth(son[rt][0],k);
if(sz[son[rt][0]]+1==k) return va[rt].x;
return kth(son[rt][1],k-sz[son[rt][0]]-1);
}
inline int nw(D x) {va[++tot]=x; sz[tot]=1; tag[tot]=ta[tot]=0; pr[tot]=rand(); return tot;}
inline void ins(D k)
{
int x,y; split(rt,x,y,k.x);
rt=merge(merge(x,nw(k)),y);
}
inline void debug(int rt)
{
if(!rt) return;
pushdown(rt);
debug(son[rt][0]); debug(son[rt][1]);
}
inline void mdf(int k)
{
int x,y,z; split(rt,x,y,k-1);
add(y,-k); ad(y,1); rt=0;
while(sz[x]&&sz[y])
{
int tmp1=kth(x,1),tmp2=kth(y,1);
if(tmp1<=tmp2) split(x,z,x,tmp2);
else split(y,z,y,tmp1);
rt=merge(rt,z);
}
if(sz[x]) rt=merge(rt,x);
if(sz[y]) rt=merge(rt,y);
}
} using namespace FHQ;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&a[i].a,&a[i].b);
sort(a+1,a+1+n,[&](const A &x,const A &y){return x.b==y.b?(x.a<y.a):(x.b>y.b);});
scanf("%d",&m);
for(int i=1,x;i<=m;i++) scanf("%d",&x),ins({x,i,0});
for(int i=1;i<=n;i++) mdf(a[i].a); debug(rt);
for(int i=1;i<=tot;i++) ans[va[i].id]=va[i].ans;
for(int i=1;i<=m;i++) printf("%d ",ans[i]);
return 0;
}
排队
仍然是 插入-标记-回收 维护函数复合。
平衡树直接做。但是线段树也可以。
注意到对于所有询问按左端点排序,那么任意时刻已加入的查询一定是单调的(不考虑右端点)。
所以我们可以对于询问开线段树,插入询问就在线段树最右面找一个点,然后映射回来。
通过线段树二分可以找到中间的合法区间,然后区间加。
比平衡树要简单的多。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+5;
int n,m,ans[N],cnt,ys[N];
struct A {int l,r;} a[N];
struct Q {bool v; int id;};
vector<Q> q[N];
namespace SEG
{
struct T {int mx,mi,lz;} tr[N<<2];
inline void pushup(int k) {tr[k].mi=min(tr[k<<1].mi,tr[k<<1|1].mi); tr[k].mx=max(tr[k<<1].mx,tr[k<<1|1].mx);}
inline void pushdown(int k)
{
if(tr[k].lz)
{
int lz=tr[k].lz; tr[k].lz=0;
tr[k<<1].mi+=lz; tr[k<<1].mx+=lz; tr[k<<1].lz+=lz;
tr[k<<1|1].mi+=lz; tr[k<<1|1].mx+=lz; tr[k<<1|1].lz+=lz;
}
}
inline void mdf(int k,int l,int r,int L,int R,int v)
{
if(l>=L&&r<=R)
{
tr[k].mi+=v; tr[k].mx+=v; tr[k].lz+=v;
return;
}
pushdown(k);
int mid=l+r>>1;
if(L<=mid) mdf(k<<1,l,mid,L,R,v);
if(R>mid) mdf(k<<1|1,mid+1,r,L,R,v);
pushup(k);
}
inline int getl(int k,int l,int r,int L,int R,int v)
{
if(l>=L&&r<=R)
{
if(tr[k].mx<v) return -1;
if(l==r) return l;
}
pushdown(k);
int mid=l+r>>1;
if(L<=mid&&tr[k<<1].mx>=v)
{
int res=getl(k<<1,l,mid,L,R,v);
if(res!=-1) return res;
}
if(R>mid&&tr[k<<1|1].mx>=v) return getl(k<<1|1,mid+1,r,L,R,v);
return -1;
}
inline int getr(int k,int l,int r,int L,int R,int v)
{
if(l>=L&&r<=R)
{
if(tr[k].mi>v) return -1;
if(l==r) return l;
}
pushdown(k);
int mid=l+r>>1;
if(R>mid&&tr[k<<1|1].mi<=v)
{
int res=getr(k<<1|1,mid+1,r,L,R,v);
if(res!=-1) return res;
}
if(L<=mid&&tr[k<<1].mi<=v) return getr(k<<1,l,mid,L,R,v);
return -1;
}
inline int que(int k,int l,int r,int p)
{
if(l==r) return tr[k].mx;
pushdown(k);
int mid=l+r>>1;
if(p<=mid) return que(k<<1,l,mid,p);
else return que(k<<1|1,mid+1,r,p);
}
} using namespace SEG;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&a[i].l,&a[i].r);
for(int i=1;i<=m;i++)
{
int l,r; scanf("%d%d",&l,&r);
q[l].push_back({0,i}); q[r+1].push_back({1,i});
} cnt=m+1;
for(int i=1;i<=n+1;i++)
{
for(auto &x:q[i])
{
if(x.v) ans[x.id]=que(1,1,m,ys[x.id]);
else ys[x.id]=--cnt;
}
if(i==n+1) break;
int l=getl(1,1,m,cnt,m,a[i].l),r=getr(1,1,m,cnt,m,a[i].r);
if(l!=-1&&r!=-1) mdf(1,1,m,l,r,1);
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}