题解/算法 {734. 能量石}

题解/算法 {734. 能量石}

LINK: https://www.acwing.com/problem/content/description/736/;

规定 最优解要选择全部的物品 (也就是, 对于价值为0的物品 我们也选上), 因此 答案为一个序列: T1, T2, ... Tn;

暴力做法 即 n ! n! n! 枚举这个答案序列, 肯定可以找到答案 因为答案就是一个序列; (这个思路非常重要, 以下的所有优化 都基于此);

但要对他做优化, 如果用线性DP 他并不符合DAG的要求 (比如对于1,6, 1可以在6的前面, 6也可以在1的前面);

-{

研究这个答案序列, 对于Ti, Tj (j=i+1), 令他们的S,E,L分表示: 该物品的耗时, 该物品最初的价值, 每秒损失的价值;
V(i)表示 序列里第i个物品的实际价值: 即max( 0, Ei - Time * Li) 其中Time表示前i-1个物品的总耗时, 注意还要有个max(0)操作 这非常重要;

pre (Ti, Tj) suf, 交换他俩的次序 并不会影响pre,suf对答案的贡献, 因此考虑贪心, 即排序;

令Time为pre的总耗时;
你很容易认为: Ti, Tj的实际价值为Vi + Vj = (Ei - Time * Li) + (Ej - (Time + Si) * Lj), 然后再得到Tj, Ti的价值, 比较他俩的大小 (可以发现 虽然Time是变量 但他会被消除掉, 结果之和S, L有关 他俩都是确定值) 然后排序, 就得到了答案序列;
. 其实这是错误的… 因为, Vi, Vj都还要进行一个max(0, ?)的操作, 换句话说 你上面得到的Vi是可能<0的, 这是错误的;

那么对形如max( 0, (Ei - Time * Li)) + max( 0, (Ej - ...))这个式子 确实又无法化简…

-}

也就是Vi + Vj = max( 0, Ri) + max( 0, Rj) (用R 来表示 未进行max(0)之前的真实值);

既然从上面已经知道, 如果Vi = Ri, Vj = Rj的话, 他是可以排序的 那么我们就只考虑这种情况;

对于答案的T1, T2, ..., Tn, 他一定是形如:>0, >0, ..., >0, 0, 0, ..., 0 即: 前面都是>0, 后面都是0 (通过反证法可以证明);
假设T1, T2, ..., Tk都是>0的, 这些物品 他们都满足Vi = Ri (即可以进行排序), 也就是 你对数组排序后 T1,T2,...,Tk 他们一定是排序后数组的一个子序列;

此时, 要看懂这个逻辑关键非常重要;
也就是, 对于T1...Tk里的任意两个元素Ta, Tb 他俩是可以进行排序的 (因为他们都不会进行max(0)操作), 由此推出 T1...Tk 在 排序后数组中 为一个子序列;
但是, 反之不然, 即对于排序后数组中的任意两个Ai, Aj 他俩在T1, T..., Tn答案序列里的次序 你是不知道的 (因为假如Ai需要涉及到max(0)操作, 那么你的排序是错误的);

令A为排序后的数组, 此时需要: 枚举A的任意子序列, 则一定可以枚举到答案T1...Tk, 但这种方法是2^n (即便你用DFS剪枝, 还是会超时);

原本他是n! (即不符合线性DP的要求), 而现在是2^n 求的是子序列 (他符合线性DP的要求), 因此 用(i,j): A[0…i]中 选择了若干个物品后, 时间为j的 最大价值;

int DP[ 102][ 10004];
void __Solve(){
    static int id = 0; ++ id;
    int ANS;
    int N;
    using Item_ = tuple< int, int, int>;
    vector< Item_> A;
    
    cin>> N;
    A.resize( N);
    for( auto & i : A) cin>> get<0>(i)>> get<1>(i)>> get<2>(i);
    sort( A.begin(), A.end(), []( const Item_ & _a, const Item_ & _b){
        auto si = get<0>( _a), li = get<2>( _a);
        auto sj = get<0>( _b), lj = get<2>( _b);
        return - si * lj > - sj * li;
    });

    // (i,j): A[0...i]中 选择了若干个物品后, 时间为`j`的 最大价值;
    constexpr int Max_time_ = 1e4;

    //> (0,?)
    memset( DP[ 0], -1, sizeof( DP[ 0]));
    DP[ 0][ 0] = 0;
    DP[ 0][ get<0>( A[0])] = get<1>( A[0]);

    //> (>0, ?)
    for( int i = 1; i < N; ++i){
        memcpy( DP[ i], DP[ i - 1], sizeof( DP[ i]));

        auto s = get<0>( A[i]), e = get<1>( A[i]), l = get<2>( A[i]);
        for( int j = 0; j + s <= Max_time_; ++j){
            if( DP[ i - 1][ j] == -1) continue;

            MAX_SELF_( DP[ i][ j + s], DP[ i - 1][ j] + max( 0, e - j * l));
        }
    }

    ANS = *max_element( DP[ N - 1], DP[ N - 1] + Max_time_);
    cout<< "Case #"<< id<< ": "<< ANS<< ED_;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值