[HAOI2010]软件安装——[缩点+树形DP]

8 篇文章 0 订阅
2 篇文章 0 订阅

【题目描述】

现在我们的手头有N个软件,对于一个软件i,它要占用Wi的磁盘空间,它的价值为Vi。我们希望从中选择一些软件安装到一台磁盘容量为M计算机上,使得这些软件的价值尽可能大(即Vi的和最大)。

但是现在有个问题:软件之间存在依赖关系,即软件i只有在安装了软件j(包括软件j的直接或间接依赖)的情况下才能正确工作(软件i依赖软件j)。幸运的是,一个软件最多依赖另外一个软件。如果一个软件不能正常工作,那么它能够发挥的作用为0。

我们现在知道了软件之间的依赖关系:软件i依赖软件Di。现在请你设计出一种方案,安装价值尽量大的软件。一个软件只能被安装一次,如果一个软件没有依赖则Di=0,这时只要这个软件安装了,它就能正常工作。

【输入格式】

第1行:N, M (0<=N<=100, 0<=M<=500)

第2行:W1, W2, … Wi, …, Wn (0<=Wi<=M )

第3行:V1, V2, …, Vi, …, Vn (0<=Vi<=1000 )

第4行:D1, D2, …, Di, …, Dn (0<=Di<=N, Di≠i )

【输出格式】
一个整数,代表最大价值。

【样例输入】
3 10
5 5 6
2 3 4
0 1 1

【样例输出】
5

【题意分析】
给你一堆软件,每个软件有重量和价值,而且有一个依赖对象,装了依赖对象才能装当前这个。

蒟蒻刚拿到这道题——这不裸的树上背包动规吗?于是暴力从没有依赖的软件(看做根节点)打了个树形DP,还过样例了。

于是自信满满地点了一下标签——缩点??!!

恍然大悟——哦!题目没说这是一棵树。。。

因此可能会存在多个环。我们想一下——一个环里的软件肯定是要么全装,要么全不装。那么我们就把一个环打包起来。。。成一个节点。剩下的就是一个DAG图(有向无环图),在这个DAG图上进行树形动规。

那么这些环将何去何从?把它们连到那里去?可以想一下,一旦撞到环了,肯定就不能往更深的地方下去了,因为它们忙着互相依赖,哪有功夫再往深的地方钻?那么我们就用简单暴力点的方法:弄一个虚拟节点0,把环和森林的根节点全都连到虚拟节点上面去。然后从虚拟节点开始树形DP。

树上背包问题:设 d p [ i ] [ j dp[i][j dp[i][j]表示目前到i节点,花费j容量获得的最大价值。
那么

d p [ i ] [ j + w [ i ] ] = m a x ( d p [ i ] [ j + w [ i ] ] , d p [ i ] [ j + w [ i ] − k ] + d p [ v ] [ k ] dp[i][j+w[i]]=max(dp[i][j+w[i]],dp[i][j+w[i]-k]+dp[v][k] dp[i][j+w[i]]=max(dp[i][j+w[i]],dp[i][j+w[i]k]+dp[v][k]

其中w[i]是当前这个节点的重量,v是枚举的每个子节点,k取遍0到j。

重新建图的时候原来的东西要清零~

Code:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<stack>
#define MAX 1000
using namespace std;

struct Front_Link_Star{
	int next,to;
}edge[MAX];

stack <int> S;
int head[MAX],w[MAX],v[MAX],W[MAX],V[MAX],DAG[MAX];
int dfn[MAX],low[MAX],indegree[MAX],n,m,tot_circle,cnt,tag;
int dp[MAX][MAX];
bool vis[MAX],check[MAX][MAX];

inline void Add_Edge(int u,int v){
	edge[++cnt].to=v;
	edge[cnt].next=head[u];
	head[u]=cnt;
}

inline int read(){
	int s=0,w=1;
	char ch=getchar();
	while (!isdigit(ch)){if (ch=='-')w=-1;ch=getchar();}
	while (isdigit(ch)){s=(s << 3)+(s << 1)+ch-'0';ch=getchar();}
	return s*w;
}

inline void tarjan(int now){
	dfn[now]=low[now]=++tag;
	vis[now]=1;S.push(now);
	for (register int i=head[now];i;i=edge[i].next){
		int v=edge[i].to;
		if (!dfn[v]){
			tarjan(v);
			low[now]=min(low[now],low[v]);
		}else if (vis[v])low[now]=min(low[now],low[v]);
	}
	if (dfn[now]==low[now]){
		++tot_circle;
		int y;
		do{
			y=S.top();
			DAG[y]=tot_circle;
			vis[y]=0;
			S.pop();
		}while (now!=y);
	}
}    //缩点

inline void DP(int now){
	for (register int i=W[now];i<=m;i++)
	    dp[now][i]=V[now];    //DP数组初始化
	for (register int i=head[now];i;i=edge[i].next){
		int v=edge[i].to;
		DP(v);    //先进儿子节点
		for (register int j=m-W[now];j>=0;j--)
		    for (register int k=0;k<=j;k++)
		        dp[now][j+W[now]]=max(dp[now][j+W[now]],dp[now][j+W[now]-k]+dp[v][k]);
	}    //暴力DP
}

int main(){
	n=read(),m=read();
	for (register int i=1;i<=n;i++)
	    w[i]=read();
	for (register int i=1;i<=n;i++)
	    v[i]=read();
	for (register int i=1;i<=n;i++){
	    int x=read();
		Add_Edge(x,i);
    }
	for (register int i=1;i<=n;i++)
	    if (!dfn[i])tarjan(i);  //缩点
	for (register int i=1;i<=n;i++){
		W[DAG[i]]+=w[i],V[DAG[i]]+=v[i];
		//节点合并
		for (register int j=head[i];j;j=edge[j].next){
		    int v=edge[j].to;
			if (DAG[i]!=DAG[v]){
				++indegree[DAG[v]];
				check[DAG[i]][DAG[v]]=true;
			} //判断是否是环。
	    }
	}       
	//重新建图。
	//本来可以直接Add_Edge(i,j),因为会对图造成影响,所以弄一个判断数组。
    cnt=0;
	memset(edge,0,sizeof(edge));
	memset(head,0,sizeof(head));
	//清零别忘了。
	for (register int i=1;i<=tot_circle;i++)
	    for (register int j=1;j<=tot_circle;j++)
	        if (check[i][j])Add_Edge(i,j);
	for (register int i=1;i<=tot_circle;i++)
	    if (!indegree[i])Add_Edge(0,i);
	//疯狂连边
	DP(0);  //从虚拟节点开始DP
	printf("%d",dp[0][m]);   //dp[0][m]为答案
	return 0;
}
这道题目还可以使用树状数组或线段树来实现,时间复杂度也为 $\mathcal{O}(n\log n)$。这里给出使用树状数组的实现代码。 解题思路: 1. 读入数据; 2. 将原数列离散化,得到一个新的数列 b; 3. 从右往左依次将 b 数列中的元素插入到树状数组中,并计算逆序对数; 4. 输出逆序对数。 代码实现: ```c++ #include <cstdio> #include <cstdlib> #include <algorithm> const int MAXN = 500005; struct Node { int val, id; bool operator<(const Node& other) const { return val < other.val; } } nodes[MAXN]; int n, a[MAXN], b[MAXN], c[MAXN]; long long ans; inline int lowbit(int x) { return x & (-x); } void update(int x, int val) { for (int i = x; i <= n; i += lowbit(i)) { c[i] += val; } } int query(int x) { int res = 0; for (int i = x; i > 0; i -= lowbit(i)) { res += c[i]; } return res; } int main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &a[i]); nodes[i] = {a[i], i}; } std::sort(nodes + 1, nodes + n + 1); int cnt = 0; for (int i = 1; i <= n; ++i) { if (i == 1 || nodes[i].val != nodes[i - 1].val) { ++cnt; } b[nodes[i].id] = cnt; } for (int i = n; i >= 1; --i) { ans += query(b[i] - 1); update(b[i], 1); } printf("%lld\n", ans); return 0; } ``` 注意事项: - 在对原数列进行离散化时,需要记录每个元素在原数列中的位置,便于后面计算逆序对数; - 设树状数组的大小为 $n$,则树状数组中的下标从 $1$ 到 $n$,而不是从 $0$ 到 $n-1$; - 在计算逆序对数时,需要查询离散化后的数列中比当前元素小的元素个数,即查询 $b_i-1$ 位置上的值; - 在插入元素时,需要将离散化后的数列的元素从右往左依次插入树状数组中,而不是从左往右。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值