• AcWing 1089 . 烽火传递


    \(AcWing\) \(1089\) . 烽火传递

    题目传送门

    一、题目大意

    烽火台是重要的军事防御设施,一般建在交通要道或险要处。

    一旦有军情发生,则白天用浓烟,晚上有火光传递军情。

    在某两个城市之间有 \(n\) 座烽火台,每个烽火台发出信号都有一定的代价。

    为了使情报准确传递,在连续 \(m\) 个烽火台中至少要有一个发出信号。

    现在输入 \(n,m\) 和每个烽火台的代价,请计算在两城市之间准确传递情报所需花费的总代价最少为多少。

    输入格式
    第一行是两个整数 \(n,m\),具体含义见题目描述;

    第二行 \(n\) 个整数表示每个烽火台的代价 \(a_i\)

    输出格式
    输出仅一个整数,表示最小代价。

    二、题目解析

    状态表示\(f[i]\)表示前\(1—i\)座烽火台满足条件,且第\(i\)座烽火台点燃的方案集合。

    属性: 所有符合条件的方案集合中的最小代价值

    状态计算

    如何划分集合?

    题目要求在连续 \(m\)个烽火台中至少要有一个发出信号,即连续的\(m\)个烽火台中至少要有一个被点燃。而\(f[i]\)表示的含义中,第\(i\)座已经被点燃,因此在第\(i\)座向前的前\(m\)座烽火台至少要有一个被点燃。被点燃的可以是\(i-m\), \(i-m+1\),...,\(i-2\),\(i -1\)

    故状态计算方程: \(f[i] = min(f[j]) + w[i] (i-m<=j<=i-1)\)

    一段区间的最值可以用单调队列求解。此题中,我们定义一个单调递增队列,队列中维护的是\(f[j]\)的最小值集合。每次拿出队头元素,即长为\(m\)的区间中,值最小的\(f[j]\)来更新答案。

    \(\large Q\):有个疑问,为什么状态表示时,要将第\(i\)座表示为点燃?

    我们可以从问题出发,每\(m\)座烽火台中必然要有一座要被点燃。那么最后\(n\)座烽火台同样也是如此。如果我们将\(f[i]\)定义为前\(1~i\)座烽火台满足条件,且第\(i\)座烽火台点燃的方案集合,那么答案一定在 \(f[n-m+1],f[n-m+2],...,f[n]\)之间产生。也就是说将第\(i\)座表示为点燃可以很容易表示出答案。这就给我们一个启发,我们在定义状态表示时,一定要考虑我们定义的状态是否可以包含答案

    一、\(DP\)+暴力查找最小值

    j = max(0, i - m)
    

    上面这句话需要特殊注意一下,别整出下标是负数的。说句人话就是:“一个看一个,个个向前看,最远能看\(m\)个,不够\(m\)个有多少算多少。”

    #include <bits/stdc++.h>
    
    using namespace std;
    const int INF = 0x3f3f3f3f;
    const int N = 200010;
    int f[N];
    int w[N], n, m;
    
    //  通过了 11/12个数据
    int main() {
        /**
        普通DP大法
        状态表示:
         ①集合:f[i]表示前i个灯塔满足条件时,并且i点亮。
         ②属性:最小代价
        状态计算:f[i]=min(f[j]+w[i]) (i−m≤j≤i−1)
        答案:(n−m+1)~n必须有灯塔亮,所以枚举一下求出最小值即可
        */
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> w[i];
    
        //初始化
        memset(f, 0x3f, sizeof f);              //预求最小,先设最大
        f[0] = 0;                               //需要手动初始化一下递推的base case
        //依题意,f[1]代表第1个灯塔点亮需要付出的代价是w[1],也就是f[0]=0,想想也正常,虚拟第0个点亮成本为0~
        for (int i = 1; i <= n; i++)
            for (int j = max(0, i - m); j <= i - 1; j++)    //向前查看它之前的m个
                f[i] = min(f[i], f[j] + w[i]);
    
        int res = INF;
        for (int i = n + 1 - m; i <= n; i++) res = min(res, f[i]);
        printf("%d\n", res);
        return 0;
    }
    

    二、滑动窗口优化的\(DP\)

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 200010;
    const int INF = 0x3f3f3f3f;
    int f[N];
    int w[N], n, m;
    int q[N];
    
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> w[i];
        int hh = 0, tt = 0;
        /*  Q:为什么这里设置tt=0,而不是 tt=-1?
            哨兵是用来解决边界值的,我们可以用tt=-1来模拟一下第一个节点按下面的代码如何执行:
            while (tt >= hh && i - q[hh] > m) hh++; 这句话不会执行,因为tt=-1 hh=0
    
            f[i] = f[q[hh]] + w[i];
            这句话会出问题,因为q队列现在是空的,q[hh]没意义,而此时f[1]按定义理解为前0个符合要求(一个都没有当然符合要求)
            第1点燃的代价,想想应该是w[1],我们虚拟前面还有一个0号节点,它的代价是0,这样这个式子处理f[1]也就通了:
            f[1]=f[0]+w[1]=w[1]
        */
        for (int i = 1; i <= n; i++) {
            //滑动窗口在i左侧,不包括i,使用前序信息可以更新f[i],滑动窗口长度最长为m
            while (hh <= tt && i - q[hh] > m) hh++;
    
            //因为i不在滑动窗口中,需要用滑动窗口的内容更新f[i],在while上方更新
            f[i] = f[q[hh]] + w[i];
    
            //i入队列
            while (hh <= tt && f[q[tt]] >= f[i]) tt--;
            q[++tt] = i;
        }
        //答案可能存在于 n-1,n-2,...n-m+1中,枚举一下求最小值即可
        int res = INF;
        for (int i = n + 1 - m; i <= n; i++) res = min(res, f[i]);
        printf("%d\n", res);
        return 0;
    }
    
  • 相关阅读:
    js-js系列-数据类型-概念
    js-基础总结3
    js-基础总结2
    js-基础总结1
    js-面试题
    webpack-模块化
    js-对象常用方法
    js-事件冒泡-事件捕获-事件委托
    js-call aplly bind2
    aioxs实现token无感刷新
  • 原文地址:https://www.cnblogs.com/littlehb/p/15816685.html
Copyright © 2020-2023  润新知