100道动态规划——41 CodeForces #419 815C Karen and Supermarket 树型DP

版权声明:也还是请注明一下原地址,虽然写的不是很好。。。 https://blog.csdn.net/Good_night_Sion_/article/details/78240862

真的,,,好久都没有写过动态规划了呀....


题意说的是对于n件物品,每一件物品都存在一个价格c和一张优惠券能够减免d元。对于第i件物品(i ≥ 2)使用优惠券时有额外的要求,那就是第xi张优惠券必须被使用,满足1<=xi<i。 给出d元金钱,求能够买的最多的物品数。


一个比较显然的事情就是这些物品使用优惠券时构成了一棵依赖树,以1号物品为root节点,这是由1<= xi <i保证的,不会出现环路。

接下来这个问题需要被转化一下,不能直接求出给出d元金钱下的最多物品数,转化为求对于确定的物品数量k,所消耗的最少的金钱数。最后求答案的时候求一个最大的k就好

首先明确一点,对于每个物品都存在两个状态,使用或者不使用优惠券。

定义状态f[u][i]表示以u为根且u使用优惠券的子树购买i件物品时使用最小花费

定义状态g[u][i]表示以u为根且u及其所有下属节点均不使用优惠券时购买i件物品的最小花费


我自己是第一次写这样的树型DP....要维护一个当前树的size(一下简写为sz),意思就是一开始当前树仅仅只有自己一个根节点,然后不断把子树合并上来来扩充sz


首先,对于任何一个节点,按照定义,初始状态都有:

f[u][1]=c[i]-d[i],g[u][1]=c[i]

f[u][0]=g[u][0]=0

sz[u]=1


每一次考虑u的一个子节点v,用一个二重循环枚举在当前u上拿i件物品,在其新子树(此处的新仅仅指没有还没有加入到u中)v上拿j件物品,来更新f[u][i+j],g[u][i+j],然后把v合并到u中。

状态转移方程写出来就是

f[u][i+j]=min(f[u][i+j],f[u][i]+min(f[v][j],g[v][j]))

g[u][i+j]=min(g[u][i+j],g[u][i]+g[v][j])


注意的一点就是在计算f[u][i+j]的时候,若i==0,不满足我们的定义,因此此时的代价应该是0x3f3f3f3f

此外一个小trick就是在每次做完当前树u之后,对于所有的1<= i <= sz[u],令f[u][i]=min(f[u][i],g[u][i])

即可在后续计算中把状态转移方程f[u][i+j]=min(f[u][i+j],f[u][i]+f[v][j])

结尾的附录写出了关于时间复杂度的分析

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int maxm=5010;

struct Edge{
    int nt,v;
    explicit Edge(int a=0,int b=0):nt(a),v(b){};
}edge[maxm];

int n,b,c[maxm],d[maxm],sz[maxm],fi[maxm],rec,f[maxm][maxm],g[maxm][maxm];
inline void addeage(int x,int y);
void dfs(int now);

int main(){
    ios_base::sync_with_stdio(0);
    cin>>n>>b>>c[1]>>d[1];
    for(int i=2,tmp;i<=n;++i)
        cin>>c[i]>>d[i]>>tmp,addeage(tmp,i);
    dfs(1);
    for(int i=n;i>=0;--i)
    if(f[1][i]<=b)
        cout<<i<<endl,i=0;
    return 0;
}

void addeage(int x, int y){
    edge[++rec]=Edge(fi[x],y);
    fi[x]=rec;
}

void dfs(int now){
    memset(f[now],0x3f,sizeof f[now]);
    memset(g[now],0x3f,sizeof g[now]);
    sz[now]=1,g[now][1]=c[now],f[now][1]=c[now]-d[now],g[now][0]=f[now][0]=0;

    for(int i=fi[now],v=edge[i].v;i;i=edge[i].nt,v=edge[i].v){
        dfs(v);

        for(int i=sz[now];i>=0;--i)
        for(int j=sz[v];j;--j)
            g[now][i+j]=min(g[now][i+j],g[now][i]+g[v][j]),
            f[now][i+j]=min(f[now][i+j],i?f[now][i]+f[v][j]:0x3f3f3f3f);
        sz[now]+=sz[v];
    }
    for(int i=1;i<=sz[now];++i)
        f[now][i]=min(f[now][i],g[now][i]);
}


appendix:有关于时间复杂度的分析:

时间复杂度实际上是O(n²)的

咋一看对于一个节点应该是n²的计算量,由于对n个节点都计算一次,最后应该是n³的复杂度。

让我们来详细分析一下

对于一个节点的计算的分析:(为了方便起见,以下的sz[1],sz[2].....sz[k]表示当前被计算节点的第XX个子树) 

计算量是:sz[1]*1 + sz[2]*(sz[1]+1) + sz[3]*(sz[2]+sz[1]+1)....... --> ∑s[i]*s[j]  +∑s[i]   (1<= j < i )  -->( (1 + ∑sz[i])² - ∑sz[i]²  - 1)/2  (注意平方的位置)

没错,对于一个节点来说计算量分析出来确实是n²的

把式子改写一下,忽略常数项有

(sz[当前树])² -  ∑sz[某颗子树]² 

当我们把所有节点的计算加到一起的时候,发现里面项是可以相互抵消的,因此总时间复杂度还是n²

Q.E.D

猜你喜欢

转载自blog.csdn.net/Good_night_Sion_/article/details/78240862