A.Binary Tree
题意
根为(a,b),则左孩子为(a+b,b),右孩子为(a,a+b)。给定(m,n),初始根为(1,1),从(1,1)到(m,n)需要往左子树走几次,往右子树走几次。
解题思路
思路一:逆向思维,从(m,n)到(1,1)。给定(m,n),求其父亲,如果m>n,则他父亲是(m-n,n),否则(m,n-m)。但是这种方法会超时。
思路二:用除法代替减法,得到的商即为往左走的次数,最后的m=m%n。n>m时情况类推。需要特别注意的是:如果m>n,m%n == 0 怎么办?因为根(1,1)不可能有0存在,所以特殊处理一下:次数:m/n-1;m=1
代码
#include <iostream>
using namespace std;
int main(){
int T, a, b, lcnt, rcnt;
cin >> T;
for(int i = 1; i <= T; ++i) {
cin >> a >> b;
lcnt = rcnt = 0;
while(a != 1 || b != 1) {
if(a >= b) {
if(a % b) {
lcnt += a/b; //优化减法,一次除法操作避免反复的减法
a %= b;
}
else{
lcnt += a/b-1;
a = 1;
}
}
else{
if(b % a) {
rcnt += b/a;
b %= a;
}
else{
rcnt += b/a-1;
b = 1;
}
}
}
cout << "Scenario #" << i << ':' << endl;
cout << lcnt << ' ' << rcnt << endl <<endl;
}
return 0;
}
B.Falling leaves
题意
给定一棵二叉搜索树以及相关的一系列操作:
(1) 删除叶子结点并给出删除的数据;
(2) 重复此过程,直至二叉树为空。
从左下角的叶子结点开始进行上述操作,得到一系列该二叉树的结点序列。根据给出的这几行树结点序列(树与树之间以“*”为间隔,输入结束标志为“$”),求该二叉树先序遍历的结点序列。
解题思路
根据给出的几行结点序列,先建树,然后再进行先序遍历。
代码
#include<cstdio>
#include<cstring>
using namespace std;
struct node{
char c;
int lch, rch;
}tree[30];
char s[30][30];
int e;
void add(int n, char c)
{
if(tree[n].c > c) {
if(!tree[n].lch) {
tree[e].c = c;
tree[n].lch = e++;
}
else add(tree[n].lch, c);
}
else {
if(!tree[n].rch) {
tree[e].c = c;
tree[n].rch = e++;
}
else add(tree[n].rch, c);
}
}
void pre_order(int n)
{
printf("%c",tree[n].c);
if(tree[n].lch) pre_order(tree[n].lch);
if(tree[n].rch) pre_order(tree[n].rch);
}
int main()
{
int cnt = 0;
while(~scanf("%s",s[cnt++])) {
if(s[cnt-1][0] == '*' || s[cnt-1][0] == '$') {
e = 0;
memset(tree,0,sizeof(tree));
int n = cnt-1;
tree[e++].c = s[n-1][0];
for(int i = n-2; i >= 0; i--) {
int len = strlen(s[i]);
for(int j = 0; j < len; j++)
add(0,s[i][j]);
}
pre_order(0);
printf("\n");
if(s[cnt-1][0] == '$') break;
cnt = 0;
}
}
return 0;
}
C.Tree Recovery
题意
此题大概意思就是给你一颗二叉树的前序遍历序列和一棵树的中序遍历序列,现在让你求出这棵树的后序遍历序列。
解题思路
这题比较简单了,就是递归的思路,前序遍历序列的第一个结点是根,根结点会把中序遍历序列分为2部分(可能某部分为空),左边的就是左子树的中序遍历结点序列,右边的就是右子树的中序遍历结点序列,这样也就确定了左子树和右子树的结点数目,根据左右子树结点数目,就可得到左右子树的先序遍历结点序列,从递归解决问题。
代码
#include <iostream>
using namespace std;
#define N 27
char pre[N],in[N];//pre表示前序遍历序列,in表示中序遍历序列
int id[N];
void print(int a,int b,int c,int d)//a,b,c,d分别表示前序和中序遍历序列的起点和终点
{
int i=id[pre[a]-'A'];//根节点
int j=i-c;//中序遍历序列的左子树
int k=d-i;//中序遍历序列的右子树
if(j) print(a+1,a+j,c,i-1);//左子树非空则递归左子树
if(k) print(a+j+1,b,i+1,d);//右子树非空则递归右子树
printf("%c",pre[a]);
}
int main()
{
while(~scanf("%s%s",pre,in))
{
for(int i=0;in[i];i++)
id[in[i]-'A']=i;
int len=strlen(in);
print(0,len-1,0,len-1);
puts("");
}
return 0;
}
D.Entropy
题意
本题就是求Huffman树的带权路径长度,然后输出原字符串占用空间(原每个字符占用8位),压缩后占用空间(带权路径长度——当直接用出现次数表示频率时),以及两个值之比。
所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。
解题思路
举个栗子:ABBCCC
权值分别为1,2,3。
先把A,B,生成一个树,此时A对应的编码为0,B为1,ABB则为011,为三位长度,再把此树和C合并的时候,A,B编码长度都增加了1,此时A为00,B为01,ABB编码长度增加的长度就是1+2(也就是第一次合并那个树的权值)。
所以用这种思想可以不用去建哈夫曼树,直接用优先队列去存权值,每次把两个最小的权值加起来(a+b),加在sum上,然后再把(a+b)压入队列。
代码
#include<queue>
#include<functional>
#include<algorithm>
#include<vector>
#include<iostream>
#include<string>
using namespace std;
template <typename T> void print_queue(T& q){
while(!q.empty()){
std::cout<<q.top()<<" ";
q.pop();
}
std::cout<<std::endl;
}
int main(){
string s;
while(getline(cin,s) && s!="END"){
//先把字符串按字典序排序,统计同一种字符出现的次数
std::sort(s.begin(),s.end());
// cout<<s<<endl;
priority_queue<int,vector<int>,greater<int>>q;//以greater作为Compare的参数保证通过优先队列的top()操作取得的元素都是队列当前最小的元素
int cnt = 1;//cnt用于统计扫描的字符出现的次数
for( int i = 0; i < s.length();++i){
if(s[i]!=s[i+1]){
q.push(cnt); //如果当前字符与前一个字符不是同一个字符,那么表明前一个字符的次数统计已完成,可以将这个字符的出现次数放入优先队列中
cnt = 1; //并将统计字符次数的变量重新置一表示统计一个新的字符出现的次数
}else{
++cnt; //当前字符与前一个字符进行比较,如果是同一个字符那么就将用于统计当前字符的次数加一
}
}
// print_queue(q);
int leng = 0;
//处理退化的情况,只有一种字符输入的情况
if(q.size()==1)
leng = q.top();
while(q.size()!=1){ //注意哈夫曼树合并完成后优先队列只有一个元素了,并且这个元素就是哈弗曼树的根节点
int min_1 = q.top();
q.pop(); //可以理解为从哈夫曼森林中取出权重最小的两棵树
int min_2 = q.top();
q.pop();
q.push(min_1+min_2); //将取出的两棵权重最小的哈弗曼树合并为一棵权重为这两棵树权重之和的树并将这棵树加入到哈夫曼森林中
leng += (min_1+min_2); //关于计算整个字符串哈夫曼编码长度的方法确实不是那么好理解.
//某一种字符在这个哈夫曼编码长度中所占的长度为其出现的次数和其在哈夫曼树中的路径的长度
//假设这个字符出现的频次为w,其在这棵哈夫曼树中的路径长度为h.
//那么在构建这一棵哈夫曼编码树的过程中,这个字符结点(可以单独作为一个哈夫曼树的根节点或者作为合并后的哈夫曼树的叶子结点)
//在会出现h次从优先队列中取出,合并再加入到优先队列中.
//语句"leng += (min_1+min_2);"(min_1+min_2)中必然包含了这个字符出现的频次w,并且执行h次.
//按此方法分析其他字符,都满足这个规律.
//从而在合并哈夫曼树的过程中,就完成了哈夫曼编码字符串长度的计算.
}
// cout<<leng<<endl;
// cout<<8.0*s.length()/leng<<endl;
printf("%d %d %.1f\n",8*s.length(),leng,8.0*s.length()/leng);
}
}
E.BST
题意
对于一个满二叉树,给所有节点按从左到右的顺序从1开始编号,使其成为一棵二叉查找树。现每次给定一个节点编号,问以该节点为根的子树的最小编号和最大编号节点分别为多少号。
解题思路
该题考查了满二叉树的基本性质,由观察可知,某个数x所处的层数k等于这个数的二进制最右边1的位置,比如12,它的二进制是1100,最右边的1的位置是3,所以12在的层数是k = 3,又由满二叉树的性质可得:该树的节点数等于2^k - 1,所以它的左右子树的节点数位(2^k - 1 - 1) / 2= 2^(k - 1) - 1,又由于2^(k - 1) = lowbit(x).左子树的节点的范围[min,x-1],右子树的范围是[x+1,max],所以min = x - lowbit(x) + 1,max = x + lowbit(x) +1.
注:lowbit()函数用来采取一个二进制最低位的一与后面的零组成的数
代码
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main(){
int n;
scanf("%d",&n);
while(n--){
int x;
scanf("%d",&x);
int t = x & (-x);//二进制的负数为正数取反加一。
//一个数与它的负数进行“与”运算即实现lowbit()函数
printf("%d %d\n",x-t+1,x+t-1);
}
return 0;
}
F.食物链
题意
求假话的数量。
解题思路
维护每一个动物的三种关系:自己同类的,吃自己的,被自己吃的。如果用三个数组来来维护,即:A表示同类,B表示被自己吃的,C表示吃自己的,那么这样一来,就不能用同一个pre数组来维护了,因为A[i],B[i],C[i]这三个集合的意义是不一样的,但是pre[i] 却只有一种意思。怎么办呢?其实很简单,为了解决pre的重复问题,我们开一个三倍于N的一维数组就行了。其中1N表示同类,N+1N+N表示被1N对应的动物所吃,2N+12N+N表示吃1~N对应的动物,这样一来,我们就可以用一个pre数组来维护三者之间的关系了。
此题还得注意的是:只有一组数据输入,否则会WA。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 100010
using namespace std;
int pre[maxn*3];
int N,K;
int find(int x){
int t=x;
while(pre[t]!=t) t=pre[t];
while(x!=t) pre[x]=t, x=pre[x];
return t;
}
int judge(int a,int b,int c){
if(a==2 && b==c) {return 0;} //如果b、c是b吃c的关系并且b和c是同一个物种,那么肯定是假话
if(b>N || c>N) {return 0;} //如果b和c超出了规定的最大物种范围,那么也可以判定是假话。
int fb=find(b),fc=find(c);
if(a==1){//当X与Y是同类时
if(fb==find(c+N) || fb==find(c+2*N)){ return 0;}//如果b被c吃或者吃c,那么也是假话
//下面的三步操作是在数组中将b和c变成同类
pre[fb]=fc;
pre[find(b+N)]=find(c+N);
pre[find(b+2*N)]=find(c+2*N);
return 1;
}
if(fb==fc || fb==find(c+N)) {return 0;}//如果b和c与另离歌相同的物种是同类,那么说明b和c是同类,这时如果b吃c,那么为假话
//接下来的三步操作是在数组中将b和c的关系变成b吃c;
pre[fb]=find(c+2*N);
pre[find(b+N)]=fc;
pre[find(b+2*N)]=find(c+N);
return 1;
}
int main(){
cin>>N>>K;
int a,b,c,ans=0;
for(int i=0;i<=3*N;i++) pre[i]=i;//初始化pre[]数组
for(int i=0;i<K;i++){
scanf("%d %d %d",&a,&b,&c);
if(judge(a,b,c)) continue;
ans++;
}
cout<<ans<<endl;
return 0;
}
G.Fence Repair
题意
农夫要收钱锯下几段特定长度的木头,每次锯的费用都等于当前所锯木头的长度,求如何能花费最少的钱完成锯木头的任务。
解题思路
每次锯下的木头的长度都是下一次锯的木头的长度的和,这很明显是哈夫曼树建树的操作,但是给出的数据是无序的,我们每次都要找出最小的两个元素,这个过程很浪费时间,所以我们不能单纯使用数组来存储输入数据。
在本题中,我们可以用优先队列实现哈夫曼树,优先队列可以使用STL容器中的priority_queue,也可以自己利用堆的思想创建一个优先队列。
代码
#include<stdio.h>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long l1;
int n,L[20001];
//这里使用优先队列的思想
priority_queue<int, vector<int>,greater<int> > que; //优先队列原为从大到小取值,这里改为从小到大
int main(){
while(scanf("%d",&n)!=EOF){
for(int i=0;i<n;i++){
scanf("%d",&L[i]);
que.push(L[i]);
}
l1 ans=0;
while(que.size()>1){//这里不能用que.empty()判断
int num1=que.top();
que.pop();
int num2=que.top();
que.pop();
que.push(num1+num2);
ans+=(num1+num2);
}
printf("%lld\n",ans);
}
return 0;
}
H.Color a Tree
题意
给定一棵树,包括树中的节点数,每个节点的权值,以及每条边的情况。现在要给这棵树涂颜色。要求如下:
1)若给一棵树涂颜色,则其父节点必须已经涂完,树根必须首先涂颜色;
2)从时间1开始开始涂颜色,每涂完一个节点才能涂下一个节点,且每个节点的涂色时间为1个单位。
现在规定整个涂色过程的总权值为所有节点涂色时间点和其权值乘积之和。
解题思路
首先我们知道由于条件1)则涂色顺序必定为拓扑序列。若没有1)的限制,那么题意就成为了有n个顶点,已知每个顶点的权值,现在要涂色,并使总权值最小。那么贪心策略在显然不过了,即:权值最大的放在前面。正是有了条件1)的限制,所以涂色顺序必须准守一定规则——即拓扑序列。这里我们可以这样猜想:如果父节点必须要在儿子节点之前涂色,那么权值最大的儿子节点就理应紧跟在父节点后别涂色,这样才能最大可能的和没有条件1)的最优情况靠近。
于是我们得出第一条猜想: 权值最大的节点必定紧跟在其父节点被涂色后涂色。
下面是证明:这里令S表示权值最大的节点,其父节点记作P,假设涂P后先涂S,然后涂其它K个节点,即时间顺序为:PSn1……nK,花费时间为:
F1= T×Cp + (T+1)×Cs + {sigma[(T+1+i)×Cni],i=1->k}
而如果涂完S后,接着涂色的是K个节点,即时间顺序为:Pn1n2……nkS,花费时间为:
F2= T×Cp + {sigma[(T+i)×Cni],i=1->k} + (T+k+1)×Cs
F1-F2= {sigma(Cni),i=1->k} - k × Cs
因为S是树中权值最大的非根结点,
所以Cni<=Cs, {sigma(Cni),i=1->k}<=k×Cs,
所以F1-F2 < =0
k个结点n1,n2,…,nk的权值不会大于Cs,因为树中权值最大的非根结点为s, 而根节点的权值可能大于Cs,但由于p的存在所以p可能是根节点, n1,n2,…,nk不可能是根节点。
故得证,树中权值最大的非根结点为p,p的父结点为q,那么为q染色之后, 在下一个时间点为p染色,这样可以使得总费用最小。
这样以后我们就可以将权值最大的非根节点p与其父节点q合并为一个节点。合并后的新节点权值为(Cp+Cq)/2,父亲节点为q的父节点,儿子节点为q的儿子节点和p的儿子节点组合而成。
证明如下:
假设现在有两个选择:一是对q和p染色,然后对非q的后代的k个结点染色;
二是对非q的后代的k个结点(n1,n2,…,nk)染色,然后对q和p染色。
第二种选择相对于第一种选择费用之差为:
F2-F1=(Cq+Cp)×k-{sigma(Cni),i=1->k}×2
也就是说,第二种方案先对k个节点染色,相比第一种方案提前了2个时间点,
那么节省的费用是2×{sigma(Cni),i=1->k}; 后对q和p染色,
相比第一种方案延后了k个时间点,增加的费用是(Cq+Cp)×k。
哪一种选择会得到最优的结果?
(F2-F1)/(2×k) = (Cq+Cp)/2 - {sigma(Cni),i=1->k}/k
如果将q和p合并为一个结点x,Cx=(Cp+Cq)/2,n1,…,nk合并为一个节点y,
Cy={sigma(Cni),i=1->k}/k,那么我们当前面对的问题就是先染x还是先染y。
显然先染权值大的点然后染权值小的点,会得到最优解。
通过上式可知,使用结点权值的算术平均值作为合并得到的新结点的权值。
根据结论2的证明结论,每一次合并时,通过计算两个结点包含的原树中所有
结点的权值的算术平均值作为新的整体结点的权值。
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 1010
struct Node{ // 邻接表
int index; // 节点序号
struct Node *next; // 指向下一个儿子节点
}node[Max];
int num[Max]; //记录节点个数,包括合并后的节点个数
int pre[Max]; //记录节点父节点
int value[Max]; //记录节点权值
bool flag[Max]; //标记是否已经合并
int n,r; // 节点个数、根节点编号
int Sum; // 最少总权值
void add(int a,int b){ //构造邻接表,表中记录着节点a的儿子节点
struct Node *temp=(struct Node*)malloc(sizeof(struct Node));
temp->index=b;
temp->next=node[a].next;
node[a].next=temp;
}
int find(){ //查找value[i]/num[i]最小值标号,注意不能是根节点
double tt=-1.0;
int index;
for(int i=1;i<=n;i++)
if(!flag[i] && double(value[i])/num[i]>tt && i!=r){
tt=double(value[i])/num[i];
index=i;
}
return index;
}
void merge(int pra,int son){ // 合并父节点和子节点,成为新节点,同时更新子节点儿子节点的父亲节点情况
num[pra]+=num[son]; //合并后的节点数量
value[pra]+=value[son]; //合并后的节点权值
struct Node *point=node[son].next;
while(point!=NULL){ //更新子节点儿子节点父节点情况
pre[point->index]=pra;
point=point->next;
}
}
void cal(){ // 贪心算法求最少总权值
for(int i=1;i<n;i++){ //需要合并n-1次
int index=find(); // 查找最大值value[i]/num[i]
flag[index]=true; //标记已经合并
int pra=pre[index]; //求其父节点
while(flag[pra]) //直到没有合并为止
pra=pre[pra];
Sum+=(value[index]*num[pra]); //增加合并后的权值
merge(pra,index);//合并父节点和子节点
}
Sum+=value[r]; //最后由于所有除开根节点的节点的权值是实际上是乘以(实际路径-1),故还需要加上所有权值之和以及根节点的权值,而此时value正好为所有节点最初时的权值之和+根节点权值,故这里要加上
}
int main(){
while(scanf("%d%d",&n,&r),n|r){
for(int i=1;i<=n;i++){
scanf("%d",&value[i]); //输入节点权值
num[i]=1; //初始个数为1
node[i].next=NULL; // 初始化为NULL,头插法建立邻接表
}
int aa,bb;
memset(flag,0,sizeof(flag)); //初始化为未合并
for(int i=1;i<n;i++){ //输入边信息
scanf("%d%d",&aa,&bb);
add(aa,bb); //建立邻接表
pre[bb]=aa;
}
Sum=0; //初始化为0
cal(); //贪心计算最下总权值
printf("%d\n",Sum);//输出
}
return 0;
}