POI 1998 最轻的语言 The lightest language
从描述中看出,非前缀且权值和最小,于是我想到了哈夫曼编码。类似的,对于这道题,我们可以考虑建立一个k叉树,这个树的每个叶节点代表一个字符串,只须找出最小的n个叶节点的权值,就是结果。
首先建立一个0节点,在此基础上首先加入每个单个字母,当叶节点的个数不足n个时,取出最小的一个节点,对这个节点进行扩展,即把它的权值分别加上每个字母的权值,作为新的k个叶节点。否则把前n小的节点取出,计算它们的权值之和。如果这个和小于当前已知最优解,更新最优解,否则结束,因为再扩展一定不会比当前值小。
实际上我们并不需要建立树结构,只需要对叶节点的权值抽象的维护。首先把每个字母的权值插入优先队列,每次取出前n小的值计算,并以最小值来维护。
一般情况下我们用最小堆实现优先队列,但是堆中可能会存在n^2个元素,大大超过了空间限制。但是优先队列中只有前n小的元素是对我们有用的,我们可以以此改进堆,只存储前n小的元素。当插入一个新的元素时,把堆中的最大值删除,这样维护的一定是前n小的。这样又会带来新的问题,在最小堆中取最大值是O(n)时间复杂度的。于是必须同时维护一个最大堆,使取删最大值变为O(logn)。需要同时维护两个堆,在每个元素之间建立映射关系,每次删除都要对两个堆进行维护。
这样虽然可以解决问题,但是会大大增加编程的复杂度,可以考虑不用堆来维护。我用了更强大的数据结构,平衡树。平衡树在插入、取最大、取最小、删除上均有O(logn)的时间复杂度,使用平衡树可以使编程思路更清晰。我用的是Treap。
以下是我用Treap实现的代码。
/*
* Problem: POI1998 naj
* Author: Guo Jiabao
* Time: 2008.12.3 20:03:22
* State: Solved
*/
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
using namespace std;
class tTreap
{
public:
class tNode
{
public:
int v,fix;
tNode *l,*r;
tNode(int tv,tNode *nl): v(tv),l(nl),r(nl)
{
fix=rand();
}
};
tNode *root,*null;
int sum,size,lim;
tTreap()
{
root=null=new tNode(0,0);
sum=0;
}
void rot_r(tNode *&p)
{
tNode *q=p->l;
p->l=q->r;
q->r=p;
p=q;
}
void rot_l(tNode *&p)
{
tNode *q=p->r;
p->r=q->l;
q->l=p;
p=q;
}
void _ins(tNode *&p,int v)
{
if (p==null)
{
p=new tNode(v,null);
}
else if (v <= p->v)
{
_ins(p->l,v);
if (p->l->fix < p->fix)
rot_r(p);
}
else
{
_ins(p->r,v);
if (p->r->fix < p->fix)
rot_l(p);
}
}
void ins(int v)
{
_ins(root,v);
size++;
sum+=v;
if (size>lim)
delmax();
}
int _delmin(tNode *&p)
{
if (p->l==null)
{
int v=p->v;
tNode *t=p;
p=p->r;
delete t;
return v;
}
else
{
return _delmin(p->l);
}
}
int _delmax(tNode *&p)
{
if (p->r==null)
{
int v=p->v;
tNode *t=p;
p=p->l;
delete t;
return v;
}
else
{
return _delmax(p->r);
}
}
int delmin()
{
size--;
int r=_delmin(root);
sum-=r;
return r;
}
int delmax()
{
size--;
int r=_delmax(root);
sum-=r;
return r;
}
};
const int MAXK=30;
const int INF=0x7FFFFFFF;
tTreap T;
int W[MAXK];
int N,K,Ans;
void init()
{
int i;
freopen("naj.in","r",stdin);
freopen("naj.out","w",stdout);
scanf("%d%d",&N,&K);
T.lim=N;
for (i=1;i<=K;i++)
{
scanf("%d",&W[i]);
T.ins(W[i]);
}
Ans=INF;
}
void solve()
{
int i,A;
while (T.sum<Ans)
{
if (T.size>=N)
{
if (T.sum<Ans)
Ans=T.sum;
else
break;
}
A=T.delmin();
for (i=1;i<=K;i++)
{
T.ins(A+W[i]);
}
}
}
int main()
{
init();
solve();
printf("%d",Ans);
return 0;
}
<h2><span class="mw-headline">最轻的语言</span></h2>
字母表AK由英文字母表的大写字母K组成。一个被称作重量的正整数被委派给字母表的每一个字母。一个由字母表AK的字母组成的字符串的重量是这个字 符串的所有字母的总重量。一个基于字母表AK的语言由这个字母表的字母组成的字符串的有限集合。一个语言的重量是所有它的字符串的重量之和。如果这个语言 的任意两个字符串W、V,W不是V的前缀,那我们说这个语言不是前缀的。
我们想找出基于字母表AK的N个要素的非前缀语言的可能的最小重量。
例如
假设K=2,字母a的重量W(a)=2 字母b的重量W(b)=5。字符串ab的重量W(ab)=2+5=7。 W(aba)=2+5+2=9。语言J={ab,aba,b}的重量-W(J)=21。语言J不是非前缀,因为字符串ab是aba的前缀。基于字母表A2最轻的三元非前缀语言(假设字母的重量如前)是{b,aa,ab};它的重量是16。
任务
写一个程序:
- 从文本文件中读取两个整数n,k和字母表Ak 的k个字母的重量;
- 计算基于字母表Ak 的非前缀的n元语言的最小重量;
把结果写到文本文件。
输入
在输入文件的首行有两个被空格号分开的正整数n、k(2<=n<=10000, 2<=k<=26)。它们是语言中的字符串数和字母表中各个字母数。第二行包括被空格号隔开的k个正整数。每一个都不大于10000。第I个数是第I个字母的重量。
输出
在输出文件的首行应该写一个整数-基于字母表Ak 的最轻的非前缀的n元语言重量。
样例输入
3 2 2 5
样例输出16