题目描述
某购物城有m个商铺,现决定举办一场活动选出人气最高店铺。
活动共有n位市民参与,每位市民只能投一票,但1号店铺如果给该市民发放 q 元的购物补贴,该市民会改为投1号店铺。
请计算1号店铺需要最少发放多少元购物补贴才能成为人气最高店铺(即获得的票数要大于其他店铺),如果1号店铺本身就是票数最高店铺,返回0。
输入描述
第一行为小写逗号分割的两个整数n,m,其中:
- 第一个整数n表示参与的市民总数
- 第二个整数m代表店铺总数
- 1 ≤ n,m ≤ 3000
第2到n+1行,每行为小写逗号分割的两个整数p,q,表示市民的意向投票情况,其中每行的:
- 第一个整数p表示该市民意向投票给p号店铺
- 第二个整数q表示其改投1号店铺所需给予的q元购物补贴
- 1 ≤ p ≤ m
- 1 ≤ g ≤ 10^9
不考虑输入的格式问题
输出描述
1号店铺需要最少发放购物补贴金额
用例
输入 | 5,5 2,10 3,20 4,30 5,40 5,90 |
输出 | 50 |
说明 |
有5个人参与,共5个店铺。 如果选择发放 10元+40元=50元 的补贴来抢2,5号店铺的票,总共发放了50元补贴(抢了5号店铺的票后,现在1号店铺只要2票就能胜出) 所以最少发放50元补贴 |
输入 | 5,5 2,10 3,20 4,30 5,80 5,90 |
输出 | 60 |
说明 |
有5个人参与,共5个店铺。 如果选择发放 10元+20元+30元=60元 的补贴来抢2,3,4号店铺的票,总共发放了60元补贴(5号店铺有2票,1号店铺要3票才能胜出) 如果选择发放 10元+80元=90元 的补贴来抢2,5号店铺的票,总共发放了90元补贴(抢了5号店铺的票后,现在1号店铺只要2票就能胜出) 所以最少发放60元补贴 |
题目解析
本题就是
的换皮题,具体解析请看该博客的。
JS算法源码
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
void (async function () {
// 市民数量, 店铺数量
const [n, m] = (await readline()).split(",").map(Number);
// shop_costs是一个字典,key为店铺id,value为该店铺名下的市民
const shop_costs = {};
// 记录所有市民中最大的改选花费,用于后面构造权值线段树
let max_cost = 0;
// 记录自己店铺胜选所需要的花费
let ans = 0;
// 获取n行输入
for (let i = 0; i < n; i++) {
// 店铺id, 改选自己店铺的花费
const [shop, cost] = (await readline()).split(",").map(Number);
// 如果改选花费为0,则说明选的就是自己店铺
if (cost == 0 || shop == 1) continue;
// 记录不选自己店铺的市民中最大的改选花费
max_cost = Math.max(max_cost, cost);
// ans记录的是自己店铺胜选所需要的花费,初始时,我们假设让所有不选自己店铺的人都改选自己店铺,那么自己店铺就获得所有选票,此时肯定胜选
ans += cost;
// shop_costs是一个字典,key为店铺id,value为该店铺名下的市民
if (!shop_costs[shop]) shop_costs[shop] = [];
shop_costs[shop].push(cost);
}
// scan是扫描线矩阵
const scan = [];
for (let costs of Object.values(shop_costs)) {
costs.sort((a, b) => b - a);
for (let i = 0; i < costs.length; i++) {
if (scan.length <= i) scan.push([]);
scan[i].push(costs[i]);
}
}
// 我手中的票,初始时,假设我们想获得所有票
let my_ticket_count = n;
// 获得所有票的花费
let my_ticket_cost = ans;
// 构建权值线段树
const tree = new WSegmentTree(max_cost);
// 开始扫描
for (let i = 0; i < scan.length; i++) {
// 被扫描线扫到的都是每个店铺名下改选花费最大的市民,对于这部分市民,我们放弃
const line = scan[i];
for (let cost of line) {
// 将被放弃的市民的改选费用加入权值线段树,后面如果自己店铺票数不够,还需要复活一些市民
tree.add(1, 1, max_cost, cost);
// 由于放弃了这部分市民,因此可以去除他们的改选花费
my_ticket_cost -= cost;
// 由于放弃了这部分市民,因此相应票数也要去除
my_ticket_count -= 1;
}
let extra_ticket_cost = 0;
// 此时其他店铺的选票数为i+1,因此如果我们的选票数my_ticket_count <= i+1,则无法取胜
if (my_ticket_count <= i + 1) {
// 我们手中总票数只有达到 i+2 张,才能战胜其他店铺,但是注意 i+2 不能超过总票数n, 因此我们还需要正确额外的 min(i+2, n) - my_ticket_count 张票
const extra_ticket_count = Math.min(i + 2, n) - my_ticket_count;
// 高效的求解一组数中的前x小数之和,可以基于权值线段树求解,相当于求解前extra_ticket_count小之和
extra_ticket_cost = tree.query(1, 1, max_cost, extra_ticket_count);
}
// 每轮扫描线扫描后,计算其花费,保留最小花费作为题解
ans = Math.min(ans, my_ticket_cost + extra_ticket_cost);
}
console.log(ans);
})();
// 权值线段树
class WSegmentTree {
constructor(n) {
// 线段树都要开4n的空间
this.count = new Array(n << 2).fill(0); // 权值树
this.sum = new Array(n << 2).fill(0); // 和树
}
/**
* 向权值线段树中加入一个数
* @param {*} k 当前所在的线段树节点的序号
* @param {*} l 前所在的线段树节点 对应的区间范围的左边界
* @param {*} r 当前所在的线段树节点 对应的区间范围的右边界
* @param {*} val 要加入权值线段数的数
*/
add(k, l, r, val) {
// 到达叶子节点
if (l == r) {
this.count[k] += 1;
this.sum[k] += val;
return;
}
// 节点k的左子节点序号
const lson = k << 1; // 相当于 2 * k
const rson = (k << 1) | 1; // 相当于 2 * k + 1
// 左子节点对应区间范围[l, mid]
// 右子节点对应区间范围[mid+1, r]
const mid = (l + r) >> 1;
if (val <= mid) {
// 要加入的数val,在左子节点区间范围内
this.add(lson, l, mid, val);
} else {
// 要加入的数val,在右子节点区间范围内
this.add(rson, mid + 1, r, val);
}
// 回溯统计
this.count[k] = this.count[lson] + this.count[rson];
this.sum[k] = this.sum[lson] + this.sum[rson];
}
/**
* 求解前rank小数之和
* @param {*} k 当前所在的线段树节点的序号
* @param {*} l 当前所在的线段树节点 对应的区间范围的左边界
* @param {*} r 当前所在的线段树节点 对应的区间范围的右边界
* @param {*} rank 求解前rank小数之和的rank值
* @returns 前rank小数之和
*/
query(k, l, r, rank) {
// 到达叶子节点
if (l == r) {
return rank * l;
}
// 节点k的左子节点序号
const lson = k << 1;
// 节点k的右子节点序号
const rson = (k << 1) | 1;
// 左子节点对应区间范围[l, mid]
// 右子节点对应区间范围[mid+1, r]
const mid = (l + r) >> 1;
// this.count统计的是某区间范围内元素的数量,这些数量累加起来就是对应元素的排名
if (this.count[lson] < rank) {
// 如果左子节点的元素数量 < rank,那么说明前rank个数,还有部分在右子节点中
return (
this.sum[lson] + this.query(rson, mid + 1, r, rank - this.count[lson])
);
} else if (this.count[lson] > rank) {
// 如果左子节点的元素数量 > rank,那么说明前rank个数都在左子节点中,需要继续分解
return this.query(lson, l, mid, rank);
} else {
// 如果左子节点元素数量 == rank,那么说明前rank个数就是左子节点内的元素,此时要求前rank小数之和,其实就是this.sum[lson]
return this.sum[lson];
}
}
}
Java算法源码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in).useDelimiter("[n,]");
// 市民数量
int n = sc.nextInt();
// 店铺数量
int m = sc.nextInt();
// shop_costs是一个字典,key为店铺id,value为该店铺名下的市民
HashMap<Integer, ArrayList<Integer>> shop_costs = new HashMap<>();
// 记录所有市民中最大的改选花费,用于后面构造权值线段树
int max_cost = 0;
// 记录自己店铺胜选所需要的花费
long ans = 0;
// 获取n行输入
for (int i = 0; i < n; i++) {
// 店铺id, 改选自己店铺的花费
int shop = sc.nextInt();
int cost = sc.nextInt();
// 如果改选花费为0,则说明选的就是自己店铺
if (cost == 0 || shop == 1) continue;
// 记录不选自己店铺的市民中最大的改选花费
max_cost = Math.max(max_cost, cost);
// ans记录的是自己店铺胜选所需要的花费,初始时,我们假设让所有不选自己店铺的人都改选自己店铺,那么自己店铺就获得所有选票,此时肯定胜选
ans += cost;
// shop_costs是一个字典,key为店铺id,value为该店铺名下的市民
shop_costs.putIfAbsent(shop, new ArrayList<>());
shop_costs.get(shop).add(cost);
}
// scan是扫描线矩阵
ArrayList<ArrayList<Integer>> scan = new ArrayList<>();
for (ArrayList<Integer> costs : shop_costs.values()) {
costs.sort((a, b) -> b - a);
for (int i = 0; i < costs.size(); i++) {
if (scan.size() <= i) scan.add(new ArrayList<>());
scan.get(i).add(costs.get(i));
}
}
// 我手中的票,初始时,假设我们想获得所有票
int my_ticket_count = n;
// 获得所有票的花费
long my_ticket_cost = ans;
// 构建权值线段树
WSegmentTree tree = new WSegmentTree(max_cost);
// 开始扫描
for (int i = 0; i < scan.size(); i++) {
// 被扫描线扫到的都是每个店铺名下改选花费最大的市民,对于这部分市民,我们放弃
ArrayList<Integer> line = scan.get(i);
for (Integer cost : line) {
// 将被放弃的市民的改选费用加入权值线段树,后面如果自己店铺票数不够,还需要复活一些市民
tree.add(1, 1, max_cost, cost);
// 由于放弃了这部分市民,因此可以去除他们的改选花费
my_ticket_cost -= cost;
// 由于放弃了这部分市民,因此相应票数也要去除
my_ticket_count -= 1;
}
int extra_ticket_cost = 0;
// 此时其他店铺的选票数为i+1,因此如果我们的选票数my_ticket_count <= i+1,则无法取胜
if (my_ticket_count <= i + 1) {
// 我们手中总票数只有达到 i+2 张,才能战胜其他店铺,但是注意 i+2 不能超过总票数n,
// 因此我们还需要正确额外的 min(i+2, n) - my_ticket_count张票
int extra_ticket_count = Math.min(i + 2, n) - my_ticket_count;
// 高效的求解一组数中的前x小数之和,可以基于权值线段树求解,相当于求解前extra_ticket_count小之和
extra_ticket_cost = tree.query(1, 1, max_cost, extra_ticket_count);
}
// 每轮扫描线扫描后,计算其花费,保留最小花费作为题解
ans = Math.min(ans, my_ticket_cost + extra_ticket_cost);
}
System.out.println(ans);
}
}
// 权值线段树
class WSegmentTree {
int[] count; // 权值树
int[] sum; // 和树
public WSegmentTree(int n) {
// 线段树都要开4n的空间
this.count = new int[n << 2];
this.sum = new int[n << 2];
}
/**
* 向权值线段树中加入一个数
*
* @param k 当前所在的线段树节点的序号
* @param l 当前所在的线段树节点 对应的区间范围的左边界
* @param r 当前所在的线段树节点 对应的区间范围的右边界
* @param val 要加入权值线段数的数
*/
public void add(int k, int l, int r, int val) {
// 到达叶子节点
if (l == r) {
this.count[k] += 1;
this.sum[k] += val;
return;
}
// 节点k的左子节点序号
int lson = k << 1; // 相当于 2 * k
// 节点k的右子节点序号
int rson = k << 1 | 1; // 相当于 2 * k + 1
// 左子节点对应区间范围[l, mid]
// 右子节点对应区间范围[mid+1, r]
int mid = (l + r) >> 1;
if (val <= mid) {
// 要加入的数val,在左子节点区间范围内
this.add(lson, l, mid, val);
} else {
// 要加入的数val,在右子节点区间范围内
this.add(rson, mid + 1, r, val);
}
// 回溯统计
this.count[k] = this.count[lson] + this.count[rson];
this.sum[k] = this.sum[lson] + this.sum[rson];
}
/**
* 求解前rank小数之和
*
* @param k 当前所在的线段树节点的序号
* @param l 当前所在的线段树节点 对应的区间范围的左边界
* @param r 当前所在的线段树节点 对应的区间范围的右边界
* @param rank 求解前rank小数之和的rank值
* @return 前rank小数之和
*/
public int query(int k, int l, int r, int rank) {
// 到达叶子节点
if (l == r) {
return rank * l;
}
// 节点k的左子节点序号
int lson = k << 1;
// 节点k的右子节点序号
int rson = k << 1 | 1;
// 左子节点对应区间范围[l, mid]
// 右子节点对应区间范围[mid+1, r]
int mid = (l + r) >> 1;
// this.count统计的是某区间范围内元素的数量,这些数量累加起来就是对应元素的排名
if (this.count[lson] < rank) {
// 如果左子节点的元素数量 < rank,那么说明前rank个数,还有部分在右子节点中
return this.sum[lson] + this.query(rson, mid + 1, r, rank - this.count[lson]);
} else if (this.count[lson] > rank) {
// 如果左子节点的元素数量 > rank,那么说明前rank个数都在左子节点中,需要继续分解
return this.query(lson, l, mid, rank);
} else {
// 如果左子节点元素数量 == rank,那么说明前rank个数就是左子节点内的元素,此时要求前rank小数之和,其实就是this.sum[lson]
return this.sum[lson];
}
}
}
Python算法源码
# 权值线段树
class WSegmentTree:
def __init__(self, n):
# 线段树都要开4n的空间
self.count = [0] * (n << 2) # 权值树
self.sum = [0] * (n << 2) # 和树
def add(self, k, l, r, val):
"""
向权值线段树中加入一个数
:param k: 当前所在的线段树节点的序号
:param l: 当前所在的线段树节点 对应的区间范围的左边界
:param r: 当前所在的线段树节点 对应的区间范围的右边界
:param val: 要加入权值线段数的数
"""
if l == r: # 到达叶子节点
self.count[k] += 1
self.sum[k] += val
return
# 节点k的左子节点序号
lson = k << 1
# 节点k的右子节点序号
rson = k << 1 | 1
# 左子节点对应区间范围[l, mid]
# 右子节点对应区间范围[mid+1, r]
mid = (l + r) >> 1
if val <= mid:
# 要加入的数val,在左子节点区间范围内
self.add(lson, l, mid, val)
else:
# 要加入的数val,在右子节点区间范围内
self.add(rson, mid + 1, r, val)
# 回溯统计
self.count[k] = self.count[lson] + self.count[rson]
self.sum[k] = self.sum[lson] + self.sum[rson]
def query(self, k, l, r, rank):
"""
求解前rank小数之和
:param k: 当前所在的线段树节点的序号
:param l: 当前所在的线段树节点 对应的区间范围的左边界
:param r: 当前所在的线段树节点 对应的区间范围的右边界
:param rank: 求解前rank小数之和的rank值
:return: 前rank小数之和
"""
if l == r: # 到达叶子节点
return rank * l
# 节点k的左子节点序号
lson = k << 1
# 节点k的右子节点序号
rson = k << 1 | 1
# 左子节点对应区间范围[l, mid]
# 右子节点对应区间范围[mid+1, r]
mid = (l + r) >> 1
# this.count统计的是某区间范围内元素的数量,这些数量累加起来就是对应元素的排名
if self.count[lson] < rank:
# 如果左子节点的元素数量 < rank,那么说明前rank个数,还有部分在右子节点中
return self.sum[lson] + self.query(rson, mid + 1, r, rank - self.count[lson])
elif self.count[lson] > rank:
# 如果左子节点的元素数量 > rank,那么说明前rank个数都在左子节点中,需要继续分解
return self.query(lson, l, mid, rank)
else:
# 如果左子节点元素数量 == rank,那么说明前rank个数就是左子节点内的元素,此时要求前rank小数之和,其实就是this.sum[lson]
return self.sum[lson]
# 市民数量,店铺数量
n, m = map(int, input().split(","))
# 记录店铺和其名下的市民
shop_costs = {}
# 记录最大的改选花费,用于后面构造权值线段树
max_cost = 0
# 记录自己店铺胜选所需要的花费
ans = 0
# 获取n行输入
for _ in range(n):
# 店铺id, 改选自己店铺的花费
shop, cost = map(int, input().split(","))
# 如果改选花费为0,则说明选的就是自己店铺
if cost == 0 or shop == 1:
continue
# 记录不选自己店铺的市民中最大的改选花费
max_cost = max(max_cost, cost)
# ans记录的是自己店铺胜选所需要的花费,初始时,我们假设让所有不选自己店铺的人都改选自己店铺,那么自己店铺就获得所有选票,此时肯定胜选
ans += cost
# shop_costs是一个字典,key为店铺id,value为该店铺名下的市民
shop_costs.setdefault(shop, [])
shop_costs[shop].append(cost)
# scan是扫描线矩阵
scan = []
# 初始化扫描线矩阵
for costs in shop_costs.values():
costs.sort(reverse=True)
for i in range(len(costs)):
if len(scan) <= i:
scan.append([])
scan[i].append(costs[i])
# 我手中的票,初始时,假设我们想获得所有票
my_ticket_count = n
# 获得所有票的花费
my_ticket_cost = ans
# 构建权值线段树
tree = WSegmentTree(max_cost)
# 开始扫描
for i, line in enumerate(scan):
# 被扫描线扫到的都是每个店铺名下改选花费最大的市民,对于这部分市民,我们放弃
for cost in line:
tree.add(1, 1, max_cost, cost)
# 由于放弃了这部分市民,因此可以去除他们的改选花费
my_ticket_cost -= cost
# 由于放弃了这部分市民,因此相应票数也要去除
my_ticket_count -= 1
extra_ticket_cost = 0
# 此时其他店铺的选票数为i+1,因此如果我们的选票数my_ticket_count <= i+1,则无法取胜
if my_ticket_count <= i + 1:
# 我们手中总票数只有达到 i+2 张,才能战胜其他店铺,但是注意 i+2 不能超过总票数n, 因此我们还需要正确额外的 min(i+2, n) - my_ticket_count 张票
extra_ticket_count = min(i+2, n) - my_ticket_count
# 高效的求解一组数中的前x小数之和,可以基于权值线段树求解,相当于求解前extra_ticket_count小之和
extra_ticket_cost = tree.query(1, 1, max_cost, extra_ticket_count)
# 每轮扫描线扫描后,计算其花费,保留最小花费作为题解
ans = min(ans, my_ticket_cost + extra_ticket_cost)
print(ans)
免责声明:
1、IT资源小站为非营利性网站,全站所有资料仅供网友个人学习使用,禁止商用
2、本站所有文档、视频、书籍等资料均由网友分享,本站只负责收集不承担任何技术及版权问题
3、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除下载链接并致以最深的歉意
4、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责
5、一经注册为本站会员,一律视为同意网站规定,本站管理员及版主有权禁止违规用户
6、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和IT资源小站的同意
7、IT资源小站管理员和版主有权不事先通知发贴者而删除本文
评论0