RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 988979
Accepted
Dmitry Chebakov
Dmitry Chebakov
Asked:2020-06-04 07:20:41 +0000 UTC2020-06-04 07:20:41 +0000 UTC 2020-06-04 07:20:41 +0000 UTC

用线段覆盖一组点的问题(或背包问题)

  • 772

前端开发者的Yandex.Blitz竞赛最近结束了,我想分析一个问题。(我再次重申,比赛已经结束)。在我看来,这个问题介于背包问题和用线段覆盖一组点之间。这是它的完整措辞:

有一个 devops Petya。在工作中,他需要在接下来的 100 天内在某些日子值班。彼佳乘地铁上班。订阅票被引入地铁,从第一次乘坐之日起的一定天数内有效。门票的持续时间越长,每天的费用就越低。我们需要帮助 Petya 省钱,并在考虑到他的值班时间的情况下,提前三个月计算出他需要购买哪些票,以使他们的总成本尽可能低。彼佳也不喜欢随身携带很多票,如果有几种最低成本相同的票,那么彼佳需要一张票少的。如果有多个这样的选项(最低成本和门票数量相同),那么其中任何一个都适合 Petya。getCheapestTickets(days, tickets),它将 Petya 的值班表 ( days) 和可能的订阅票选项 ( tickets) 作为输入,并作为输出给出 Petya 需要购买的票列表(以票选项输入数组中的索引的形式)。Petya 的值班时间表以排序后的数字数组形式给出(从 1 到 100 包括在内),每个数字表示值班日的序号:

// Петя должен дежурить на второй, пятый, десятый и сорок пятый день относительно текущей даты  
[2, 5, 10, 45]

每个订阅票由以下接口描述:

interface Ticket {  
    // количество дней, в течение которых билет действует со дня первой поездки по нему,  
    // включая этот день (от 1 до 100 включительно)  
    duration: number;  
    // стоимость билета (от 1 до 100 включительно)  
    cost: number;  
}

门票选择数量不超过10个,并保证所有门票价格不同,门票有效期越多,一日成本越低。

输入格式

days:
[1, 2, 4, 6, 7, 8, 9, 10, 20]
tickets:

[  
    { cost: 3, duration: 1 },  
    { cost: 10, duration: 7 },  
    { cost: 20, duration: 30 }  
]

输出格式

[0, 0, 1, 0]

我的问题是我的解决方案没有及时通过。首先,我尝试通过递归遍历整个决策树(JavaScript)来解决这个问题:

const getCheapestTickets = (days, tickets) => {
  // Вспомогательная функция для обхода всех вариантов билетов.
  // l указывает на текущий день из массива days.
  const wrapper = (l) => {
    let min = null;

    // Обходим все билеты.
    for (let i = 0; i < tickets.length; i += 1) {
      const ticket = tickets[i];
      let j = l;

      // Считаем количество дней, которые покрывает данный билет.
      const skip = days[l] + ticket.duration;
      while (skip > days[j]) {
        j += 1;
      }

      let curr;
      if (j < days.length) {
        // Рекурсивно ищем решение для оставшихся дней.
        curr = wrapper(j);
        curr.sum += ticket.cost;
      } else {
        curr = { sum: ticket.cost, includes: [] };
      }

      const currIncludes = [i, ...curr.includes];
      curr = { ...curr, includes: currIncludes };

      if (min === null) {
        min = curr;
      } else if (
        (curr.sum === min.sum && curr.includes.length < min.includes.length)
        || curr.sum < min.sum) {
        min = curr;
      }
    }

    return min;
  };

  return wrapper(0, 0).includes;
};

在这里,我不明白如何丢弃决策分支。我看不出如何将其简化为经典的背包问题,通过矩阵寻求解决方案。接下来,我尝试编写相同的解决方案,但没有递归,也绕过所有可能的变化,但在树的帮助下。也没有及时过去。请帮助制定更优化的算法。解决方案是单线程的,这一点很重要。

алгоритм
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    Zergatul
    2020-06-07T21:51:27Z2020-06-07T21:51:27Z

    这个问题与背包问题无关。根本区别在于,在背包问题(或分段覆盖)中,元素的数量是有限的,而在我们的问题中,我们可以购买无限数量的每种类型的票。

    很容易看出,如果我们有一组天的最优解,并且可以分成 2 个部分的最优覆盖,那么我们再次得到 2 个子集的最优解。如果它们不是最优的,那么我们的原始解决方案也不是最优的:

    дни дежурства [a1, a2, ..., an, b1, b2, ..., bn]
    билета        [.t.]....[..t..]  [t]....[...t...] // оптимальное покрытие билетами
    отсюда следует, 2 других оптимальных покрытий:
    для [a1, a2, ..., an] -> [.t.] .. [..t..]
    для [b1, b2, ..., bn] -> [t]....[...t...]
    

    因此,可以使用贪心策略动态解决问题。在这种情况下,复杂性是,其中n是工作天数。

    function getCheapestTickets(days, tickets) {
    
        // функция пытается найти билет, который покроет интервал дней дежурства
        // начиная с days[from] заканчивая days[to]
        // при этом билет не должен покрывать days[from - 1] и days[to + 1]
        let getTicket = function (from, to) {
            let length = days[to] - days[from] + 1;
            let maxLength = 100;
            if (from > 0 && to < days.length - 1)
                maxLength = days[to + 1] - days[from - 1] + 1;
            for (let i = 0; i < tickets.length; i++)
                if (tickets[i].duration >= length && tickets[i].duration < maxLength)
                    return i;
            return null;
        };
    
        let solutions = {};
        // каноническое решение для покрытия нуля дней
        solutions[-1] = { cost: 0, tickets: [] };
        const maxCost = 1000000000;
        for (let i = 0; i < days.length; i++) {
            // лучшее решение для покрытия дней с days[0] до days[i]
            let bestDaySolution = { cost: maxCost };
            // проходимся по всем лучшим решениям для меньших промежутков
            // и пытаемся добавить 1 билет
            for (let j = -1; j < i; j++) {
                if (solutions[j]) {
                    let ticket = getTicket(j + 1, i);
                    if (ticket == null)
                        continue;
                    let cost = solutions[j].cost + tickets[ticket].cost;
                    let better =
                        // если цена меньше
                        cost < bestDaySolution.cost ||
                        // или если цена такая же, но количество билетов меньше
                        (cost == bestDaySolution.cost && bestDaySolution.tickets.length > solutions[j].tickets.length + 1)
                    if (better) {
                        bestDaySolution.cost = cost;
                        bestDaySolution.tickets = solutions[j].tickets.slice();
                        bestDaySolution.tickets.push(ticket);
                    }
                }
            }
            if (bestDaySolution.cost != maxCost)
                solutions[i] = bestDaySolution;
        }
    
        return solutions[days.length - 1].tickets;
    }
    
    console.log(getCheapestTickets([1, 2, 4, 6, 7, 8, 9, 10, 20], [  
        { cost: 3, duration: 1 },
        { cost: 10, duration: 7 },  
        { cost: 20, duration: 30 }  
    ]));

    • 1
  2. hedgehogues
    2020-06-27T05:07:39Z2020-06-27T05:07:39Z

    我将告诉您一项与此非常相似的任务,但是,与它不同。也许这会很有用。

    给定集合对数字的因式分解

    这个存储库是一种算法的实现,它允许您在给定的一组数字上分解一个数字。例如,我们有一组数字1, 2, 2, 3, 4, 6。我们有多少种方法可以得到一个数字5?明显地:

    1, 4
    1, 2, 2
    2, 3
    2, 3
    

    因此,我们已经通过了所有可能的解决方案。请注意,重复其中一种解决方案。考虑并避免不必要的重复很重要。

    基线

    让我们为一组数字分配一个位掩码:

    [0, 0, 1, 0] 
    

    然后,要获得解决方案,枚举所有位掩码就足够了。这种算法的复杂度为:$O(2^N)$,其中 $N$ 是掩码的大小。显然,已经有 22 个元素,您将不得不等待很长时间才能获得所有解决方案。

    这里应该注意的是,任何解决方案都可以很快找到。但是如果我们想确保没有解,证明它是唯一的,或者找到所有的,那么算法的执行时间就会被延迟。

    考虑一个我在实践中解决的应用问题。

    一个任务

    让我们去购物吧。我们知道总价格,即 所有商品的总金额和每件商品的价格。但是,客户退回了部分货物。需要了解客户购买了哪些商品。

    继续

    在某些情况下,我们想了解这个问题有多少解决方案?

    解决方案

    有货:

    在此处输入图像描述

    每一个都要花费一些东西。

    在此处输入图像描述

    还有出发和回程:

    在此处输入图像描述

    有一个解决方案:

    在此处输入图像描述

    通过在解决方案中依次包含和排除所有内容,我们可以找到所有可能的解决方案。

    优化 - 1.(背包问题)

    在这里阅读 更多 并递归迭代所有解决方案。因此,我们将回答任务的所有问题。这种方法的问题是我们不能迭代地执行它,这意味着我们必须将所有解决方案存储在内存中,而且可能有很多。

    需要注意的是,这个问题是背包问题的一个特例。背包任务如下所示:

    在此处输入图像描述

    有一个背包,它有一个容量,我们还想在其中收集最大价值的物品,以便它们都适合。同时,每个项目都有其成本和重量(限制)。一项任务中可能存在多个约束。

    В нашем случае всё проще. Во-первых, вес рюкзака и ограничение в данном случае совпадают.

    在此处输入图像描述

    Во-вторых, в отличие от задаче о рюкзаке, нам требуется составить из наших предметов в точности максимальный вес рюкзака. Не забываем, что вес совпадает с ограничением, либо сказать, что такого решения не существует.

    Оптимизация - 2

    Для того, чтобы генерировать решения итеративно, предлагается отранжировать элементы. В данном случае это возможно, покуда ограничение и вес совпадают. Затем, на каждом шаге, мы будем набивать рюкзак, итерируясь от максимального элемента к минимальному и будем пересчитывать остаток. Как только остаток станет меньше 0, возвращемся к предыдущему шагу и продолжаем поиск. Если остаток равен в точности 0, то возвращаем решение, а затем, с того же места продолжаем поиск.

    请注意,此解决方案所需的时间大约是背包问题的 2 倍。同时,我们可以迭代地给出答案。

    • 1

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    根据浏览器窗口的大小调整背景图案的大小

    • 2 个回答
  • Marko Smith

    理解for循环的执行逻辑

    • 1 个回答
  • Marko Smith

    复制动态数组时出错(C++)

    • 1 个回答
  • Marko Smith

    Or and If,elif,else 构造[重复]

    • 1 个回答
  • Marko Smith

    如何构建支持 x64 的 APK

    • 1 个回答
  • Marko Smith

    如何使按钮的输入宽度?

    • 2 个回答
  • Marko Smith

    如何显示对象变量的名称?

    • 3 个回答
  • Marko Smith

    如何循环一个函数?

    • 1 个回答
  • Marko Smith

    LOWORD 宏有什么作用?

    • 2 个回答
  • Marko Smith

    从字符串的开头删除直到并包括一个字符

    • 2 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5