buinss
题意: 有一棵
n
(
n
<
=
1
e
5
)
n(n<=1e5)
n(n<=1e5)个节点的完全二叉树,每个节点有
a
[
i
]
a[i]
a[i]个果实.
然后有
m
(
m
<
=
1
e
5
)
m(m<=1e5)
m(m<=1e5)次操作,每次操作会选取一条直上直下的链,操作有两个属性,分别是能拿走
c
[
i
]
c[i]
c[i]个果实.取走每个果实的支付的钱
w
i
wi
wi.要求为每次操作选择拿哪些果实.然后使得支付的钱最多.
部分分解法:
w
[
i
]
=
1
w[i]=1
w[i]=1
此时我们考虑每一条链,会发现尽量先取链顶深度大的,而且尽量取在较深的位置会比较优.所以我们自下而上贪心.
具体实现:先在每个操作对应链的链底挂上该操作的序号,这个用vector,然后对于每个节点再开一个multiset< pair<int,int> >,维护以深度降序的每个操作,以及这个操作还能取多少个.然后先处理两个儿子的信息,再启发式合并.对每个节点取果实都是能取完就尽量取完.
正解1:
前置部分:如果
n
<
=
2000
,
m
<
=
2000
n<=2000,m<=2000
n<=2000,m<=2000,怎么做呢?费用流就行了.
暴力代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=1e5+5;
inline int read(){
char c=getchar();int t=0,f=1;
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){t=(t<<3)+(t<<1)+(c^48);c=getchar();}
return t*f;
}
int T,n,m,a[maxn];
struct node{
int u,v,c,w;
}b[maxn];
int s,t;
struct edge{
int v,p,w,c;
}e[1000005];
int h[200005],tot=1;
inline void add(int a,int b,int c,int d){
e[++tot].p=h[a];
e[tot].v=b;
e[tot].w=c;
e[tot].c=d;
h[a]=tot;
}
int num,cost,vis[200005],ht[200005];
const int inf=0x3f3f3f3f;
int dis[200005],inq[200005];
bool bfs(){
queue<int> q;
for(int i=1;i<=t;i++)dis[i]=-inf;
for(int i=1;i<=t;i++)inq[i]=0;
while(!q.empty())q.pop();
dis[s]=0;
q.push(s);
while(!q.empty()){
int u=q.front();q.pop();inq[u]=0;
//printf("%lld %lld\n",u,dis[u]);
for(int i=h[u];i;i=e[i].p){
int v=e[i].v;
// printf("%lld %lld %lld\n",v,dis[v],dis[u]+e[i].c);
if(e[i].w&&dis[v]<dis[u]+e[i].c){
dis[v]=dis[u]+e[i].c;
if(!inq[v]){
q.push(v);
inq[v]=1;
}
}
}
}
return dis[t]!=-inf;
}
int dfs(int u,int rest){
if(u==t||rest==0)return rest;
vis[u]=1;
int tot=0;
for(int &i=ht[u];i;i=e[i].p){
int v=e[i].v;
if(((!vis[v])||v==t)&&e[i].w&&(dis[v]==dis[u]+e[i].c)){
int di=dfs(v,min(rest,e[i].w));
e[i].w-=di;e[i^1].w+=di;
rest-=di;cost+=di*e[i].c;
tot+=di;
//printf("%lld %lld %lld %lld\n",u,v,di,e[i].c);
if(0==rest)break;
}
}
return tot;
}
int dinic(){
int ans=0;
while(bfs()){
vis[t]=1;
while(vis[t]){
for(int i=1;i<=t;i++)vis[i]=0;
for(int i=1;i<=t;i++)ht[i]=h[i];
ans+=dfs(s,inf);
// printf("%lld\n",cost);
}
}
return ans;
}
signed main(){
//freopen("buinss.in","r",stdin);
//freopen("buinss.out","w",stdout);
T=read();
while(T--){
n=read(),m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=m;i++){
b[i].u=read();b[i].v=read();b[i].c=read();b[i].w=read();
}
for(int i=2;i<=tot;i++){
e[i].w=0;e[i].v=0;e[i].c=0;e[i].p=0;
}
for(int i=1;i<=n+m;i++)h[i]=0;
tot=1;num=n;cost=0;
s=n+m+1,t=n+m+2;
h[s]=0;h[t]=0;
for(int i=1;i<=n;i++){
add(i,t,a[i],0);
add(t,i,0,0);
}
for(int i=1;i<=m;i++){
num++;
add(s,num,b[i].c,0);
add(num,s,0,0);
int tmp=b[i].v;
while(tmp!=b[i].u){
add(num,tmp,b[i].c,b[i].w);
add(tmp,num,0,-b[i].w);
//printf("%lld %lld %lld\n",num,tmp,b[i].w);
tmp=tmp/2;
}
//printf("%lld %lld %lld\n",num,tmp,b[i].w);
add(num,tmp,b[i].c,b[i].w);
add(tmp,num,0,-b[i].w);
}
dinic();
//printf("%lld\n",dinic());
printf("%lld\n",cost);
}
return 0;
}
首先我们发现应该先取
w
[
i
]
w[i]
w[i]较大的.所以我们先把每个操作按
w
[
i
]
w[i]
w[i]排序.
然后相当于需要在线处理每个新来的操作,能够选走多少个果实.
这个也是自下而上贪心处理(这里其实就是模拟费用流),
这里multiset的含义是:如果可以反悔(本来一个位置是贡献给a操作的,但是发现它贡献给b操作更优,那么这个位置就要反悔),那么最多可以反悔多少(这里类似网络流中的反向边).可以发现,这个反悔只会在a操作链顶深度小于b操作的时候才会发生.所以还需要记反悔操作的链顶深度.
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=1e5+5;
inline int read(){
char c=getchar();int t=0,f=1;
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){t=(t<<3)+(t<<1)+(c^48);c=getchar();}
return t*f;
}
int n,m,T,a[maxn],u[maxn],v[maxn],c[maxn],w[maxn],dep[maxn],fa[maxn];
typedef pair<int,int> pii;
namespace sub_task{
multiset<pii> s[maxn];
vector<int> in[maxn];
int ans;
void merge(int x,int y){
if(s[x].size()<s[y].size())swap(s[x],s[y]);
for(auto t:s[y])s[x].insert(t);
}
void dfs(int x){
if((x<<1)<=n)dfs(x<<1),merge(x,x<<1);
if((x<<1|1)<=n)dfs(x<<1|1),merge(x,x<<1|1);
for(int t:in[x])s[x].insert(pii(-dep[u[t]],c[t]));
for(;(!s[x].empty())&&a[x];){
pii tmp=*s[x].begin();s[x].erase(s[x].begin());
int now=min(tmp.second,a[x]);
a[x]-=now;tmp.second-=now;ans+=now;
if(tmp.second)s[x].insert(tmp);
}
while(!s[x].empty()){
pii tmp=*s[x].begin();
if(-tmp.first==dep[x])s[x].erase(s[x].begin());
else break;
}
}
void solve(){
for(int i=1;i<=n;i++)s[i].clear(),in[i].clear();
for(int i=1;i<=m;i++)in[v[i]].push_back(i);
ans=0;dfs(1);
printf("%lld\n",ans);
}
}
namespace greedy{
multiset<pii> s[maxn];
int ord[maxn],cnt=0;
bool cmp(int x,int y){return w[x]>w[y];}
int calc(int x,int d,int c){
if(!c) return 0;
int ret=0;
if(a[x]){
int now=min(c,a[x]);
a[x]-=now;c-=now;ret+=now;
s[x].insert(pii(d,now));
}
while(c&&(!s[x].empty())){
pii tmp=*s[x].begin();
if(tmp.first<d){
s[x].erase(s[x].begin());
int now=calc(x>>1,tmp.first,min(tmp.second,c));
c-=now;tmp.second-=now;ret+=now;
if(now)s[x].insert(pii(d,now));
if(tmp.second){s[x].insert(tmp);break;}
}else break;
}
if(dep[x]>d&&c)ret+=calc(x>>1,d,c);
return ret;
}
void solve(){
for(int i=1;i<=n;i++)s[i].clear();
for(int i=1;i<=m;i++)ord[i]=i;
sort(ord+1,ord+1+m,cmp);
int ans=0;
for(int i=1;i<=m;i++)ans+=calc(v[ord[i]],dep[u[ord[i]]],c[ord[i]])*w[ord[i]];
printf("%lld\n",ans);
}
}
signed main(){
T=read();
while(T--){
n=read(),m=read();
for(int i=1;i<=n;i++)fa[i]=i>>1,dep[i]=dep[i>>1]+1;
for(int i=1;i<=n;i++)a[i]=read();
int flag=1;
for(int i=1;i<=m;i++){
u[i]=read();v[i]=read();c[i]=read();w[i]=read();
if(w[i]!=1){flag=0;}
}
if(flag)sub_task::solve();
else greedy::solve();
}
return 0;
}
正解2 dp
首先还是考虑把
w
[
i
]
w[i]
w[i]从大到小排序,然后还是在线处理每个新加入的链.
处理方法1:按顺序的每条链都尽量取的越多越好.所以我们二分这个取了多少.然后判断是否可行:根据霍尔定理,存在完美匹配的充要条件是对于其的每个子集部分都满足左侧-右侧
<
=
0
<=0
<=0,然后就可以考虑求出最大的左侧-右侧(此处的右侧指点的总果实数,左侧指每条链制定取出的数量).这个直接用dp求不太行.
所以我们转换方式,变为选一个点使得权值减去该点的果实数,然后如果一条链全选了,就把这条链全部的权值加上.
这里
d
p
[
i
]
[
l
e
n
]
dp[i][len]
dp[i][len]表示在第i个节点,已经决策了它的祖先选不选,然后从它的父亲往上连续的都被选上的节点的长度为len时,最大的左侧-右侧的值,现在我们要考虑第i个节点选不选.然后先计算出i的儿子的情况,再转移回i.
这个复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),然后我们有m条边,复杂度
O
(
n
∗
m
∗
l
o
g
n
)
O(n*m*logn)
O(n∗m∗logn),然后还有二分的复杂度…
此时十分爆炸,但是根据观察,我们发现每次加一条链,影响的只有其链底所在祖先和自己的dp值,总共logn级别的.然后我们还发现对于新来的一条链,我们一定强制选它(如果不这样做,之前的链都已经合法了,再怎么算也是合法的).所以可以把它的权值先加上一个极大数.再dp求出最值后减去这个极大值,就是这条边实际能取的值.这样复杂度
O
(
n
l
o
g
2
n
)
O(nlog^2n)
O(nlog2n)
注意:首先由于题目中数据组数可能很多,所以不能直接将数组全部清空,只能清空一部分.然后清空的范围应该是上一次的n,而不是这一次新的n.这是因为在mk函数中,f数组的更新可能会有从编号大于n的节点转移过来,如果初始化正确,这里就不会更新,但是如果初始化不正确(不完整),就会出现问题.
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=2e5+5;
inline int read(){
char c=getchar();int t=0,f=1;
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){t=(t<<3)+(t<<1)+(c^48);c=getchar();}
return t*f;
}
int T,n,m,a[maxn];
struct node{
int u,v,c,s;
}b[maxn];
bool comp(int x,int y){
return b[x].s>b[y].s;
}
int ans,dep[maxn];
typedef pair<int,int> pi;
pi dis[maxn][18],f[maxn][18];
#define mp make_pair
pi operator +(pi a,pi b){
return mp(a.first+b.first,a.second+b.second);
}
void mk(int x){
int x1=x+x,x2=x+x+1;
f[x][0]=max(f[x1][0],f[x1][1])+max(f[x2][0],f[x2][1]);
//printf("%lld %lld ",f[ls][1].first,f[rs][1].first);
for(int i=1;i<=dep[x];i++){
f[x][i]=max(f[x][i-1],max(f[x1][i+1],f[x1][0])+max(f[x2][i+1],f[x2][0]));
}
pi sum=mp(0,-a[x]);
for(int i=1;i<=dep[x];i++){
sum=sum+dis[x][i];
f[x][i]=f[x][i]+sum;
}
}
int o[maxn];
inline void modiy(int x,int y,pi a){
int dp=dep[y]-dep[x]+1;
//printf("%lld %lld %lld %lld\n",dis[y][dp].first,dis[y][dp].second,a.first,a.second);
dis[y][dp]=dis[y][dp]+a;
//printf("%lld %lld\n",dis[y][dp].first,dis[y][dp].second);
for(;y;y>>=1)mk(y);
}
signed main(){
//freopen("buinss.in","r",stdin);
//freopen("buinss1.out","w",stdout);
T=read();
while(T--){
for(int i=1;i<=n;i++)
memset(f[i],0,sizeof(f[i]));
for(int i=1;i<=n;i++)
memset(dis[i],0,sizeof(dis[i]));n=read(),m=read();ans=0;
for(int i=1;i<=n;i++)dep[i]=dep[i>>1]+1;
for(int i=1;i<=n;i++){
a[i]=read();
for(int j=1;j<=dep[i];j++)f[i][j].second=-a[i];
}
for(int i=1;i<=m;i++){
b[i].u=read(),b[i].v=read(),b[i].c=read(),b[i].s=read();
o[i]=i;
}
//printf("%lld %lld\n",f[1][0].second,f[1][1].second);
sort(o+1,o+1+m,comp);
for(int i=1;i<=m;i++){
modiy(b[o[i]].u,b[o[i]].v,mp(1,0));
pi alf=max(f[1][0],f[1][1]);int vl=min(-alf.second,b[o[i]].c);
ans=ans+vl*b[o[i]].s;
modiy(b[o[i]].u,b[o[i]].v,mp(-1,vl));
}
printf("%lld\n",ans);
}
return 0;
}