模拟费用流或dp

13 篇文章 0 订阅
2 篇文章 0 订阅

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(nmlogn),然后还有二分的复杂度…
此时十分爆炸,但是根据观察,我们发现每次加一条链,影响的只有其链底所在祖先和自己的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;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值