垃圾ACMer的暑假训练220707

垃圾ACMer的暑假训练220707

12. 贪心

12.1 魔术券

题意

商店的优惠券上印有整数 N N N(可能是负数).若将优惠券用于产品,商店会给你 N N N倍的产品价值的前.此外,商店免费提供一些赠品.若将优惠券用于赠品,你需向商店支付 N N N倍的赠品价值的钱.现给定若干优惠券和若干商品,每个优惠券和商品至多只能选择一起,问最多能从商店拿回多少钱.

第一行输入一个整数 N c    ( 1 ≤ N c ≤ 1 e 5 ) N_c\ \ (1\leq N_c\leq 1\mathrm{e}5) Nc  (1Nc1e5)表示优惠券数量.第二行输入 N c N_c Nc个整数(绝对值不超过 100 100 100),表示各优惠券上印的数值.第三行输入一个整数 N p    ( 1 ≤ N p ≤ 1 e 5 ) N_p\ \ (1\leq N_p\leq 1\mathrm{e}5) Np  (1Np1e5)表示产品数.第四行包含 N p N_p Np个整数(绝对值不超过 100 100 100),表示各产品的价值,其中商品的价值为正,赠品的价值为负.

思路

显然没必要选 N = 0 N=0 N=0的券,应选正数乘正数或负数乘负数的组合.对负数乘负数等价于都取绝对值后相乘,故只需考虑正数乘正数,排序不等式秒.

实现时先将两序列升序排列.对正数的部分,用两个指针从序列的末尾往前移,直至一个指针指向的数非正数;对负数的部分,用两个指针从序列的开头往后移,直至一个指针指向的数非负数.

最坏每个数都是 100 100 100,则最多 1 e 5 × 10 0 2 = 1 e 9 1\mathrm{e}5\times 100^2=1\mathrm{e}9 1e5×1002=1e9,不会爆int.

代码
const int MAXN = 1e5 + 5;
int n, m;  // 序列长度
int a[MAXN], b[MAXN];

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) cin >> a[i];
	cin >> m;
	for (int i = 0; i < m; i++) cin >> b[i];

	sort(a, a + n), sort(b, b + m);

	int ans = 0;
	for (int i = 0, j = 0; i < n && j < m && a[i] < 0 && b[j] < 0; i++, j++) 
		ans += a[i] * b[j];
	for (int i = n - 1, j = m - 1; i >= 0 && j >= 0 && a[i] > 0 && b[j] > 0; i--, j--) 
		ans += a[i] * b[j];
	cout << ans;
}


12.2 排成最小数 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

给定一个数组,其中包含若干个非负整数,整数可能含前导零.现需将所有数字拼起来形成一个新的数,使得该数尽可能小.

先输入一个 n    ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e}4) n  (1n1e4),表示数组中元素的个数.接下来输入 n n n个非负整数,每个数字不超过 8 8 8位,可能包含前导零.

输出能排列出的最小数,不输出前导零.

思路

定义字符串的小于等于号: s t r 1 ≤ s t r 2 ⇔ s t r 1 + s t r 2 ≤ s t r 2 + s t r 1 str1\leq str2\Leftrightarrow str1+str2\leq str2+str1 str1str2str1+str2str2+str1,其中后者比较字典序.

需验证这样定义的小于等于号可用于排序,即证明这种二元关系是全序关系.

[] (完全性) 两字符串的字典序要么 ≥ \geq ,要么 ≤ \leq ,显然成立.

(反对称性) { s 1 ≤ s 2 ⇔ s 1 + s 2 ≤ s 2 + s 1 s 2 ≤ s 1 ⇔ s 2 + s 1 ≤ s 1 + s 2 ⇒ s 1 + s 2 = s 2 + s 1 \begin{cases}s_1\leq s_2\Leftrightarrow s_1+s_2\leq s_2+s_1 \\ s_2\leq s_1\Leftrightarrow s_2+s_1\leq s_1+s_2\end{cases}\Rightarrow s_1+s_2=s_2+s_1 {s1s2s1+s2s2+s1s2s1s2+s1s1+s2s1+s2=s2+s1,这正是 s 1 = s 2 s_1=s_2 s1=s2的定义.

(传递性) 设 s 1 , s 2 , s 3 s_1,s_2,s_3 s1,s2,s3的长度分别为 x , y , z x,y,z x,y,z,且 s 1 ≤ s 2 , s 2 ≤ s 3 s_1\leq s_2,s_2\leq s_3 s1s2,s2s3.欲证 s 1 ≤ s 3 s_1\leq s_3 s1s3.

s 1 ≤ s 2 ⇔ s 1 + s 2 ≤ s 2 + s 1 s_1\leq s_2\Leftrightarrow s_1+s_2\leq s_2+s_1 s1s2s1+s2s2+s1,因 l e n ( s 1 + s 2 ) = l e n ( s 2 + s 1 ) \mathrm{len}(s_1+s_2)=\mathrm{len}(s_2+s_1) len(s1+s2)=len(s2+s1),则字典序关系与数值关系等价,

​ 即 ⇔ s 1 × 1 0 y + s 2 ≤ s 2 × 1 0 x + s 1 ⇔ s 1 ( 1 0 y − 1 ) ≤ s 2 ( 1 0 x − 1 ) ⇔ s 1 s 2 ≤ 1 0 x − 1 1 0 y − 1 ⋯ \Leftrightarrow s_1\times 10^y+s_2\leq s_2\times 10^x+s_1\Leftrightarrow s_1(10^y-1)\leq s_2(10^x-1)\Leftrightarrow \dfrac{s_1}{s_2}\leq\dfrac{10^x-1}{10^y-1}\cdots s1×10y+s2s2×10x+s1s1(10y1)s2(10x1)s2s110y110x1①.

​ 同理$s_2\leq s_3\Leftrightarrow \dfrac{s_2}{s_2}\leq\dfrac{10y-1}{10z-1}\cdots $②.

​ 欲证式 s 1 ≤ s 3 ⇔ s 1 s 2 ≤ 1 0 x − 1 1 0 z − 1 s_1\leq s_3\Leftrightarrow\dfrac{s_1}{s_2}\leq\dfrac{10^x-1}{10^z-1} s1s3s2s110z110x1.因①和②两边都为正,相乘即证.

用该定义的小于号对输入的字符串升序排列后拼在一起的数最小.

[] 若不然,设最优解的排序为 s 1 , s 2 , ⋯   , s n s_1,s_2,\cdots,s_n s1,s2,,sn,且其中至少 ∃ \exists 一对 s i > s i + 1 s_i>s_{i+1} si>si+1.

由小于等于号的定义: s i + s i + 1 > s i + 1 + s i s_i+s_{i+1}>s_{i+1}+s_i si+si+1>si+1+si.

考察 s 1 + ⋯ + s i + s i + 1 + ⋯ + s n s_1+\cdots+s_i+s_{i+1}+\cdots+s_n s1++si+si+1++sn s 1 + ⋯ + s i + 1 + s i + ⋯ + s n s_1+\cdots+s_{i+1}+s_i+\cdots+s_n s1++si+1+si++sn,

​ 两者只有 s i s_i si s i + 1 s_{i+1} si+1的部分不同,而 s i + s i + 1 > s i + 1 + s i s_i+s_{i+1}>s_{i+1}+s_i si+si+1>si+1+si,这表明前者大于后者,故前者非最优解,矛盾.

代码
const int MAXN = 1e5 + 5;
int n;
string str[MAXN];

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) cin >> str[i];

	sort(str, str + n, [](string s1, string s2) {
		return s1 + s2 < s2 + s1;
		});

	string ans = "";
	for (int i = 0; i < n; i++) ans += str[i];
	int idx = 0;  // 有效数字起始下标
	while (idx + 1 < ans.size() && ans[idx] == '0') idx++;  // 去除前导零
	cout << ans.substr(idx);
}


12.3 用Swap(0,i)排序 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

给定一个 0 ∼ ( n − 1 ) 0\sim (n-1) 0(n1)的排列,要求只用 s w a p ( 0 , i ) swap(0,i) swap(0,i)操作(将数字 0 0 0与另一数字交换位置)将该排列变为升序序列,求最小步数.

第一行输入 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5),表示排列的元素个数.第二行输入 0 ∼ ( n − 1 ) 0\sim (n-1) 0(n1)的一个排列.

思路

转化为图论问题:若数 i i i在下标 p [ i ] p[i] p[i]的位置上,则节点 i i i向节点 p [ i ] p[i] p[i]连一条边.这样每个节点的出度和入度都是一,建立的图是若干个环.将序列升序排列即将所有环变成 n n n个自环.

如:

下标 p [ i ] p[i] p[i] 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5
数值 i i i 1 1 1 3 3 3 4 4 4 0 0 0 2 2 2 5 5 5

在这里插入图片描述

s w a p ( 0 , i ) swap(0,i) swap(0,i)操作分为三种:

①若节点 i i i是与节点 0 0 0相邻的点,如 s w a p ( 0 , 3 ) swap(0,3) swap(0,3)时,序列变为:

下标 p [ i ] p[i] p[i] 0 0 0 1 1 1 3 3 3
数值 i i i 1 1 1 0 0 0 3 3 3

​ 此时 0 0 0指向的节点形成自环, 0 0 0所在的环减小:

在这里插入图片描述

②若节点 i i i非与节点 0 0 0的相邻点,但与 0 0 0在同一环内,显然这样会将 0 0 0所在的环拆开成两个小环.注意到拆开成两个小环后,仍需交换 0 0 0与其相邻的节点,让小环中的节点形成自环,显然不如直接交换 0 0 0与其相邻的节点,故非最优解.

③若节点 i i i与节点 0 0 0不在同一环中,如 s w a p ( 0 , 2 ) swap(0,2) swap(0,2)时,序列变为:

下标 p [ i ] p[i] p[i] 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4
数值 i i i 1 1 1 3 3 3 4 4 4 2 2 2 0 0 0

​ 此时节点 0 0 0 i i i所在的环合并:

在这里插入图片描述

故最优解只需做操作①③,在任意时刻将不含节点 0 0 0的非自环与节点 0 0 0所在环合并,每次 s w a p swap swap节点 0 0 0与其相邻的节点产生自环.

最坏情况是 n n n为奇数时, 0 0 0号节点自环,其余节点两两形成共 n − 1 2 \dfrac{n-1}{2} 2n1个环,此时需进行 n − 1 2 \dfrac{n-1}{2} 2n1次合并使得所有节点与节点 0 0 0在同一环中,再至多进行 ( n − 1 ) (n-1) (n1) s w a p swap swap节点 0 0 0与其相邻的节点使得其他节点形成自环,总时间复杂度 O ( n ) O(n) O(n).

代码
const int MAXN = 1e5 + 5;
int n;
int p[MAXN];  // p[i]表示数i的下标

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		int x; cin >> x;
		p[x] = i;
	}

	int ans = 0;
	int idx = 1;  // 将要与节点0交换的节点
	while (idx < n) {
		while (p[0]) swap(p[0], p[p[0]]), ans++;  // 将节点0所在的环都变成自环
		while (idx < n && p[idx] == idx) idx++;  // 跳过已经是自环的节点
		if (idx < n) swap(p[0], p[idx]), ans++;  // 将不含节点0的环与节点0合并
	}
	cout << ans;
}


12.4 月饼 ( 0.15   s ) (0.15\ \mathrm{s}) (0.15 s)

题意

给定各种月饼的存量和价值,以及市场对月饼的需求量,求最大利润.

第一行输入整数 n , d    ( 1 ≤ n ≤ 1000 , 1 ≤ d ≤ 500 ) n,d\ \ (1\leq n\leq 1000,1\leq d\leq 500) n,d  (1n1000,1d500),分别表示月饼种类和市场对月饼的需求量.第二行输入 n n n个整数,表示每种月饼的存量,每种月饼的存量不超过 200 200 200.第三行输入 n n n个整数,表示每种月饼的价值,总价值不超过 2000 2000 2000.

输出最大利润,保留两位小数.

思路

求出每种月饼的性价比,优先选性价比高的月饼.

代码
const int MAXN = 1005;
int n;  // 月饼种类数
double d;  // 市场需求量
struct Cake {
	double price, weight;

	bool operator<(const Cake& p)const { return price / weight > p.price / p.weight; }
}cakes[MAXN];

int main() {
	cin >> n >> d;
	for (int i = 0; i < n; i++) cin >> cakes[i].weight;
	for (int i = 0; i < n; i++) cin >> cakes[i].price;
	
	sort(cakes, cakes + n);

	double ans = 0;
	for (int i = 0; i < n && d > 0; i++) {
		double w = min(d, cakes[i].weight);
		d -= w;
		ans += cakes[i].price / cakes[i].weight * w;
	}
	cout << fixed << setprecision(2) << ans;
}


12.5 整数集合划分

题意

假设集合可包含重复元素.给定一个含 n    ( 2 ≤ n ≤ 1 e 5 ) n\ \ (2\leq n\leq 1\mathrm{e}5) n  (2n1e5)个正整数的集合,将其划分为两集合 A 1 A_1 A1 A 2 A_2 A2,其中 A 1 A_1 A1包含 n 1 n_1 n1个元素, A 2 A_2 A2包含 n 2 n_2 n2个元素,且 A 1 A_1 A1中的元素之和为 s 1 s_1 s1, A 2 A_2 A2中的元素之和为 s 2 s_2 s2,要求在 ∣ n 1 − n 2 ∣ |n_1-n_2| n1n2尽可能小的基础上 ∣ s 1 − s 2 ∣ |s_1-s_2| s1s2尽可能大.数据保证集合中任意元素和所有元素之和小于 2 31 2^{31} 231.

输出 ∣ n 1 − n 2 ∣ |n_1-n_2| n1n2 ∣ s 1 − s 2 ∣ |s_1-s_2| s1s2.

思路

为使得 ∣ n 1 − n 2 ∣ |n_1-n_2| n1n2尽可能小,当 n n n为偶数时,两集合各分一半;当 n n n为奇数时,一个集合在分一半的基础上多一个元素.

为使得 ∣ s 1 − s 2 ∣ |s_1-s_2| s1s2尽可能大,亦即让 ∣ s 2 − s 1 ∣ |s_2-s_1| s2s1尽可能大,可先将序列升序排列, A 1 A_1 A1选前 ⌊ n 2 ⌋ \left\lfloor\dfrac{n}{2}\right\rfloor 2n个, A 2 A_2 A2选后 ⌊ n 2 ⌋ \left\lfloor\dfrac{n}{2}\right\rfloor 2n个( n n n为偶数时)或后 ( ⌊ n 2 ⌋ + 1 ) \left(\left\lfloor\dfrac{n}{2}\right\rfloor+1\right) (2n+1)个.

代码
const int MAXN = 1e5 + 5;
int n;
int a[MAXN];

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) cin >> a[i];
	
	sort(a, a + n);
	int s1 = 0, s2 = 0;
	for (int i = 0; i < n / 2; i++) s1 += a[i];
	for (int i = n / 2; i < n; i++) s2 += a[i];
	cout << n % 2 << ' ' << s2 - s1;
}


12.6 结绳 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

给定若干段绳子,将它们串成一条绳.如下图,每次串连时将两段绳子对折后套接在一起,将这样得到的绳子视为另一段绳子,可再次对折并与另一段绳子串连.每次串连后,两段绳子长度减半.给定绳子的长度,求它们能串成的绳子的最大长度.

第一行输入整数 n    ( 2 ≤ n ≤ 1 e 4 ) n\ \ (2\leq n\leq 1\mathrm{e}4) n  (2n1e4),表示绳子段数.第二行输入 n n n个正整数,表示绳子的原始长度,所有绳子的初始长度不超过 1 e 4 1\mathrm{e}4 1e4.

输出能串连成的绳子的最大长度,答案向下取整.

思路

串连长度为 a a a b b b的两绳子得到的新绳长度为 a + b 2 \dfrac{a+b}{2} 2a+b.

Huffman树,将每段绳子视为两叶子节点,将它们串连得到父节点.显然最优解用到所有绳子,则最后的绳子是根节点对应的绳子.显然两棵二叉树结构相同时,最后得到的绳子长度相等,则长度为 l l l的绳子对最终绳子长度的贡献取决于绳子对应的节点与根节点的距离 d d d,贡献为 l 2 d \dfrac{l}{2^d} 2dl.

最优解是将长的绳子放在深度小的位置,短的绳子放在深度大的位置.

[] 设绳长 z > y z>y z>y.考虑交换两绳子对应的节点的位置,显然同深度节点交换不改变最终绳长.

z z z在上, y y y在下,则它们对最终绳长的贡献为 ( y 大 + z 小 ) \left(\dfrac{y}{大}+\dfrac{z}{小}\right) (y+z).

y y y在上, z z z在下,则它们对最终绳长的贡献为 ( y 小 + z 大 ) \left(\dfrac{y}{小}+\dfrac{z}{大}\right) (y+z).

y 大 + z 小 − y 小 − z 大 = 1 大 ( y − z ) − 1 小 ( y − z ) = ( 1 大 − 1 小 ) ( y − z ) > 0 \dfrac{y}{大}+\dfrac{z}{小}-\dfrac{y}{小}-\dfrac{z}{大}=\dfrac{1}{大}(y-z)-\dfrac{1}{小}(y-z)=\left(\dfrac{1}{大}-\dfrac{1}{小}\right)(y-z)>0 y+zyz=1(yz)1(yz)=(11)(yz)>0.

最优策略是每次串连所有绳子中长度最短的两条.

x , y x,y x,y是当前最短、次短的两段绳子,注意到 x + y 2 ≤ y \dfrac{x+y}{2}\leq y 2x+yy,故每次串连当前最短和次短的两段绳子得到的绳子都是当前最短的绳子,故无需用堆.

代码
const int MAXN = 1e5 + 5;
int n;
double a[MAXN];

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) cin >> a[i];
	
	sort(a, a + n);

	for (int i = 1; i < n; i++) a[0] = (a[0] + a[i]) / 2;  // 后面的绳子合并到最短的绳子上
	cout << (int)a[0];
}


12.7 是否加油 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

高速公路上有若干个加油站,不同加油站的油价可能不同.假设初始时油箱为空.

第一行输入四个整数 c m a x , d , d a v g , n    ( 1 ≤ c m a x ≤ 100 , 1 ≤ d ≤ 3 e 4 , 1 ≤ d a v g ≤ 20 , 1 ≤ n ≤ 500 ) c_{max},d,d_{avg},n\ \ (1\leq c_{max}\leq 100,1\leq d\leq 3\mathrm{e}4,1\leq d_{avg}\leq 20,1\leq n\leq 500) cmax,d,davg,n  (1cmax100,1d3e4,1davg20,1n500),分别表示油箱的最大容量、起点到目的地的距离、每单位汽油可供行驶的距离、加油站总数.接下来 n n n行每行输入一队非负数 p i , d i    ( 1 ≤ p i ≤ 100 , 0 ≤ d i ≤ d ) p_i,d_i\ \ (1\leq p_i\leq 100,0\leq d_i\leq d) pi,di  (1pi100,0did),分别表示每升汽油的价格、该加油站与起点的距离.

若能到达目的地,输出到达目的地的最小花费,答案保留两位小数.若无法到达目的地,输出"The maximum travel distance = x x x",其中 x x x为到达的最远距离,答案保留两位小数.

思路

因初始时油箱为空,若起点处无加油站,则无解.

每次加满油可行驶 c m a x × d a v g c_{max}\times d_{avg} cmax×davg的路程

从起点出发,在距离起点 c m a x × d a v g c_{max}\times d_{avg} cmax×davg的距离内找到第一个比起点处的加油站的油价低的加油站 A A A(若存在),则在起点处的加油站加的油只需足以行驶到该 A A A,再在 A A A处加油,以 A A A为起点继续行驶;若距离起点 c m a x × d a v g c_{max}\times d_{avg} cmax×davg的距离内无比起点处的加油站的油价更低的加油站,则选择在距离内油价最低的加油站 B B B处加油,但未必要加满,而是以该加油站为起点继续考察航程内是否有比 B B B的油价更低的加油站.

若中途的某个起点处的航程内无加油站,则无解.

将目的地视为一个油价为 0 0 0的加油站.

代码
const int MAXN = 505;
int c_max, d, d_avg, n;  // 油箱容量、起点到终点的距离、每升油能行驶的距离、加油站数
struct Stop {
	double price, dis;

	bool operator<(const Stop& p)const { return dis < p.dis; }
}stops[MAXN];

int main() {
	cin >> c_max >> d >> d_avg >> n;
	for (int i = 0; i < n; i++) cin >> stops[i].price >> stops[i].dis;
	stops[n] = { 0,(double)d };  // 终点视为油价为0的加油站

	sort(stops, stops + n + 1);

	if (stops[0].dis)  // 起点处无加油站,则无解
		return cout << "The maximum travel distance = 0.00", 0;

	double ans = 0, oil = 0;  // 花费、当前油量
	for (int i = 0; i < n;) {
		int k = -1;  // 记录下一个加油站的下标
		double max_dis = c_max * d_avg;
		for (int j = i + 1; j <= n && cmp(max_dis, stops[j].dis - stops[i].dis) >= 0; j++) {
			if (stops[j].price < stops[i].price) {  // 存在比起点加油站油价更低的加油站
				k = j;
				break;
			}
			else if (k == -1 || stops[j].price < stops[k].price)  // 不存在比起点加油站油价更低的加油站
				k = j;  // 找油价相对低的
		}

		if (k == -1) {
			printf("The maximum travel distance = %.2lf\n", stops[i].dis + max_dis);
			return 0;
		}
		
		if (stops[k].price <= stops[i].price) {  // 油只需足够行驶到油价更低的加油站
			ans += ((stops[k].dis - stops[i].dis) / d_avg - oil) * stops[i].price;
			oil = 0;  // 行驶到下个加油站刚好没油
		}
		else {
			ans += (c_max - oil) * stops[i].price;  // 加满油
			oil = c_max - (stops[k].dis - stops[i].dis) / d_avg;  // 行驶到下一个加油站
		}
		i = k;  // 以当前的加油站为起点
	}
	cout << fixed << setprecision(2) << ans;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值