算法竞赛进阶指南第五章 线性DP——Cookies

在这里插入图片描述
这是贪心和动态相结合的一道题。
要找到一种好的分配方式,使得怨气值之和最小,**贪婪度大的孩子如果能够给他尽可能多的饼干,可以尽量使得最后怨气值之和最小。**即按怨气值从大到小排序。
这样的贪心是否正确呢?——可以用邻项交换法来证明一下


设先有一种分配饼干的方案是,设g[i]<g[i+1]
1 2 3 …… i i+1 …… n (1)
1 2 3 …… i+1 i …… n (2)
交换i 和 i+1位置的孩子,发现对于前1 ~ i个 和 从i+2 ~ n个,这样的交换对于这些孩子的怨气值没有影响
有影响的就是:
(1)中去除相同的部分,不同的是:g[i+1]*1
(2) 中去除相同的部分,不同的是:g[i]*1
又因为 g[i]<g[i+1] 所以(2)中的最终怨气值会更小

不断的交换,最后会使得怨气值按从大到小的顺序排列更优

————————————————————————————

大体方案有了,具体每个孩子分多少块呢?可以借助dp来处理

定义f(i,j)表示前i个孩子分j块时,这i个孩子的怨气值总值最小。
在考虑dp的转移时,我们考虑是由那种状态能够达到f(i,j)的状态(怎么来的),也可以考虑下一个状态可以达到哪些情况(往哪去),这里我们考虑下一个状态有哪些,即第i+1个小朋友该如何分配饼干数。
→ 情况1:第i+1个小朋友分的比第i个小,则a[i+1]=i,即前面由i个小朋友拿到的饼干比他多(原因每一个小朋友饼干数的分配数量呈非严格单调下降,第i个小朋友比第i+1个多,则前面的不会小于第i个小朋友的饼干数) 如图。
在这里插入图片描述

→ 情况2:第i+1个小朋友拿到的饼干数与第i个小朋友相同,此时还需要算出前面有多少个小朋友拿到的饼干数与第i个相同。
…………………………………………………………………………
不好转移,做一个等价处理:
如果第i个拿到饼干数 > 1,处理方式为:令她及前面的孩子的饼干数均减少1个,即将j-i个饼干分给前i个孩子,因为这样处理前后的大小关系不会发现变化,从而使得怨气值之和也不会发生变化。
如果第i个拿到的饼干数=1,处理方式为:枚举她前面有多少个孩子拿的都是1个饼干,设分界点为k,即1 ~ k 个孩子拿到饼干数>1,编号k+1 ~ i的孩子饼干数=1
此情况下的状态转移方程为:
在这里插入图片描述

综上,
在这里插入图片描述

这道题启发我们,有时可以通过额外的算法确定DP状态的计算顺序,有时可以在状态空间中运用等效手法对状态进行缩放。在本题中,我们就利用贪心策略,在DP前对Ⅳ个孩子执行排序,使他们获得的饼干数单调递减。我们还利用相对大小的不变性,把第 i+1个孩子获得的饼干数先缩放到1,再考虑i前面有几个孩子获得的饼干数量相等,使需要计算的问题得到了极大的简化,容易进行维护、转移。

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

using namespace std;

typedef pair<int, int> PII;

const int N = 35, M = 5010;

int n, m;
//g[i] 表示第 i 个小孩的贪婪度
//c[i] 表示贪婪度第 i 大的小孩在原序列中排第 c[i] 个
//s[i] 表示 c[i] 的前缀和
int g[N], c[N], s[N];
int f[N][M]; //设 f[i][j] 表示前 i 个孩子一共分配了 j 块饼干时,他们的怨气总和最小值
PII pre[N][M]; //pre[i][j] 记录 f[i][j] 的前驱状态
int res[N]; //res[i] 表示最优方案中第 i 个孩子分到的饼干数

bool cmp(int a, int b) //比较函数,按照贪婪度从大到小排序
{
    return g[a] > g[b];
}

void print(PII t) //从结束状态回推整个方案
{
    int a = t.first, b = t.second;

    if(a == 0) return; //如果回推完每个人的饼干数,结束

    print(pre[a][b]); //从当前状态继续回推方案

    /*
    如果当前状态和前一个状态分糖果的人数相同,说明是 f[i][j] = f[i][j - i],
    这种情况在转移的过程中我们进行了前 i 个孩子都少一块饼干的等价转换,所以
    回推的时候需要往回转换,即前 i 个孩子增加一块饼干
    */
    if(pre[a][b].first == a)
    {
        for(int i = 1; i <= a; i++) res[c[i]]++;
    }
    else
    {
        /*
        否则说明前一个状态到当前状态的中间这段小孩都是相同饼干数,由于递归进
        去再出来,所以这一段在整个序列的最末尾,这一段小孩的饼干数对当前状态
        来说可以取最小,即取 1
        */
        for(int i = pre[a][b].first + 1; i <= a; i++) res[c[i]] = 1;
    }
}

int main()
{
    scanf("%d%d", &n, &m);

    for(int i = 1; i <= n; i++) scanf("%d", &g[i]);

    for(int i = 1; i <= n; i++) c[i] = i; //初始化映射

    sort(c + 1, c + 1 + n, cmp); //所有小孩按照贪婪度从大到小排序

    for(int i = 1; i <= n; i++) s[i] = s[i - 1] + g[c[i]]; //预处理前缀和

    //初始化
    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;

    //dp
    for(int i = 1; i <= n; i++)
        for(int j = i; j <= m; j++) //每个小孩至少一个饼干,前 i 个小孩至少 i 个饼干
        {
            //状态 1
            f[i][j] = f[i][j - i];
            pre[i][j] = {i, j - i};

            //状态 2
            for(int k = 0; k < i; k++) //枚举有多少个小孩的饼干数 > 第 i 个小孩
                if(f[i][j] > f[k][j - (i - k)] + (s[i] - s[k]) * k) //如果当前状态的怨气总和能更小
                {
                    //更新
                    f[i][j] = f[k][j - (i - k)] + (s[i] - s[k]) * k;
                    pre[i][j] = {k, j - (i - k)};
                }
        }
    printf("%d\n", f[n][m]); //输出最小怨气总和

    print({n, m}); //从结束状态回推整个方案

    for(int i = 1; i <= n; i++) printf("%d ", res[i]); //输出每个小孩的饼干数

    return 0;
}

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
算法竞赛进阶指南》是一本进阶级别的书籍,不适合初学者阅读。根据引用中的描述,每一章都会总结书中的难点知识,并附上例题和习题。从引用的目录可以看出,《算法竞赛进阶指南》包含了基本算法、位运算、递推与递归、前缀和与差分、二分、排序、倍增、贪心等内容,还包括了基本数据结构如栈、队列、链表、Hash、字符串、Trie、二叉堆等。此外,书中还讲解了动态规划的各种子领域,如线性dp、背包、区间dp、树形dp、状态压缩dp等。对于想要深入学习算法竞赛的读者来说,《算法竞赛进阶指南》是一本很好的参考书籍。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [【算法竞赛进阶指南】学习笔记](https://blog.csdn.net/cpp_juruo/article/details/122520206)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [算法竞赛进阶指南总结(一)](https://blog.csdn.net/weixin_64393298/article/details/124234703)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值