1.2贪心
注意事项
贪心条件:
- 每次的选择不能依赖后面要做的选择
- 它的最优解包含其子问题的最优解
贪心的证明:见书
例题:【例题4】荷马史诗
- 题目抽象:
给定n个单词的出现次数,求将n个单词改为n个k进制字符串后使文章最短,且对于任意的 1 ≤ i , j ≤ n , i ≠ j 1\le i,j\le n,i\ne j 1≤i,j≤n,i=j都有 s i s_i si不是 s j s_j sj的前缀,求文章的最短长度和最长的 s i s_i si的最短长度 - 解析:
- 利用树的思想,建立一个k叉树,边权表示字符对应的数字,点权表示该字符串出现的次数,深度表示字符的长度
- 用大根堆维护k进制数的出现次数,每次取出前k小的k进制数,给他们接一个新的公共的父亲,用这个父亲代替他们重新放入堆中,进行以后的合并,不断循环至只有一个点无父亲(此为根节点)
- 父节点的建立:点权为它的子的点权之和,深度为它的子的深度的最大值+1
- 在建树的过程中将所以节点的出现次数相加,和为文章的总长度
- 根节点的深度为最长字符串的最短长度
- 重点:
- 本题并不用建真正的树,只是利用思想进行合并,且节点的深度与树中节点的深度恰好相反
- 因为深度代表字符串长度,在建父节点时深度要加1
- 为方便建树,要让 ( n − 1 ) m o d ( k − 1 ) = = 0 (n-1)mod(k-1)==0 (n−1)mod(k−1)==0,否则用权值为0的点补全
- 用结构体+优先队列存点代码如下:
struct node{
ll v;
ll d;
inline bool operator < (const node& b) const{
if(v!=b.v) return v>b.v;
else return d>b.d;
}
};
priority_queue<node>q;
- 重载运算符时注意要让大根堆的top(即最大值)为次数最小的点,若次数相同要深度最小的
- 用cnt记录剩余节点数,先初始为 ≥ n \ge n ≥n的(k-1)的倍数,每次合并 -=(k-1),当cnt==1时合并结束
- 代码实现:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 100;
ll n, k, w[N], cnt, ans;
struct node {
ll v;
ll d;
inline bool operator<(const node& b) const {
if (v != b.v)
return v > b.v;
else
return d > b.d;
}
};
priority_queue<node> q;
int main() {
#ifndef ONLINE_JUDGE
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
#endif
scanf("%lld%lld", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%lld", &w[i]);
node tmp;
tmp.v = (ll)w[i];
tmp.d = 0; //初始:n个叶子,子树dep==0,权值为w[i]
q.push(tmp);
}
cnt = n;
if ((n - 1) % (k - 1)) { //若n-1不是k-1的整数倍,就用权值为0的点补全
cnt += (k - 1) - ((n - 1) % (k - 1));
}
for (int i = 1; i <= cnt - n; i++) {
node tmp;
tmp.v = 0ll;
tmp.d = 0;
q.push(tmp);
}
while (cnt != 1) { //当无父亲的节点不止一个时,即树未建完
ll val = 0ll;
ll dep = 0;
for (int i = 1; i <= k; i++) {
node tot = q.top();
val += (ll)tot.v;
dep = max(dep, tot.d);
q.pop();
}
cnt -= k - 1;
ans += val;
node tmp;
tmp.v = val;
tmp.d = dep + 1; //新建父亲,子树高度是子的最大高度+1
q.push(tmp);
}
printf("%lld\n%lld\n", ans, q.top().d);
return 0;
}
终于写完了~~
为什么要有这样让人崩溃的题啊