HDUOJ 1754 - I hate it 与线段树解法

# HDUOJ 1754 - I hate it 与线段树解法

这个题目是中文描述的,理解起来也没有难度。于是在2012-06-06 10:38:48我提交了第一份代码。结果很让人沮丧,Time Limit Exceeded。
渣代码如下:

#include <stdio.h>
#define MAXN 200000
#define MAXM 5000
int ScoreTable[MAXN + 1];
int main()
{
	int n, m;
	int i;
	int maxScore;
	int a, b;
	char command[4];
	while(scanf("%d%d", &n, &m) == 2)
	{
		for(i = 1; i <= n; i++)
		{
			scanf("%d", ScoreTable + i);
		}
		for(i = 0; i < m; i++)
		{
			scanf("%s%d%d", command, &a, &b);
			if(command[0] == 'Q')
			{
				maxScore = ScoreTable[a];
				for(a = a + 1; a <= b; a++)
				{
					if(ScoreTable[a] > maxScore)
					{
						maxScore = ScoreTable[a];
					}
				}
				printf("%d\n", maxScore);
			}
			else/* 'U' */
			{
				ScoreTable[a] = b;
			}
		}
	}
	return 0;
}



说它完全没算法也不对,至少它将学生信息在数组中的存储位置与该学生的ID信息关联起来,ID即数组索引时的下标。

这时问题来了,更新操作时依据ID可以直接找到成绩,O(1)的复杂度!但是查询操作需要依次从给定的A至B逐个比较过来才能得出结论,记n=B-A+1,即需要遍历的学生个数,那么其时间复杂度为O(n)。如果每次查询都是从1至N,那么总的运行时间复杂度就是O(N*M)。

既然重复遍历很浪费时间,那么有没有可能将前面查询的结果存起来呢?比如之前查询过前1至N/2的MAX,那么下次再问1至N的MAX,不就是只需要跳过前N/2个成绩,只需遍历一半就可?

沈永超说何不将原始区间分成10、100、1000等份,提前计算出这些区间的MAX值。好提议。问题是心中的想法不明确时,拿不准到底要分成几份。况且如果输入数据N较大时,分成10份,也只能在查询本身恰好铺满10份中的某块时才有提速,那些没有铺满的区间意味着只能遍历去找MAX。况且原始区间可不一定是10的整数倍,划分出的最后的那个区间的匹配问题好像会很罗嗦。既然区间划分数量太小会导致需要遍历地去找未铺满划分区间的情况,那么反之将原始区间分成很多很多份会怎么样呢?极端点的看,把原始长度为N的区间分成N个长度为1的小区间,那么现在虽然总是铺满划分的子区间,但是多出了各个子区间的本身的max的遍历才能从max1,max2,max3,...maxn中得出最终的MAX!

结合曾经听过的“区间树”,能否用树状结构对区间进行划分呢?最简单的树要数二叉树了。那么该怎么构造呢?经过一番冥思苦想,百度“区间树”不可得之后,终于在纸上画出了我想要的对区间进行分割的树!


附一张从百度百科摘来的图:


简单的说就是二叉树划分区间,最后分到区间长度到1为止。

查找时从根开始,分两种情况:

1. 所求区间正好匹配,直接取该节点记录的max值;
2. 对当前节点区间对半分,分成左右两部分,于是所求区间有位置有三种情况:
    1. 全部落在左边,那么相当于求取左侧子区间的匹配max;
    2. 全部落在右边,那么相当于求取右侧子区间的匹配max;
    3. 分别落在左右两侧,那么意味着求左侧子区间匹配max1,以及右侧子区间匹配max2,这两者中的较大者。

由于区间最终被划分到长度为1,所以总是在情况1——所求区间正好匹配——时结束递归。

至于更新的操作,为了免去在树中查找长度为1的叶子节点,可以在构造这棵树的时候就按照下标(ID)索引叶子节点记录好。因此为了更新下标i的元素,根据预先创建的叶子节点索引直接找出叶子节点。于是有如下递归操作:

1. 新值是否与原来的max相同,如果相同,那么更新操作到此结束;否则更新本节点的max后需要向上传递;
2. 本节点收到更新请求,那么需要查看左右两个子节点的max记录,才能得出本节点应该被更新为多少,回到情况1;
3. 当前节点已经是树根时,更新请求不得不停止向上传递。

说了这么多纯脑子里思考的东西,下面直接上代码:

#include <stdio.h>
#include <stdlib.h>
#define MAXN 200000
#define MAXM 5000

struct node
{
    int max;
    int lindex;
    int rindex;
    struct node *lchild;
    struct node *rchild;
    struct node *parent;
};

int ScoreTable[MAXN + 1];
struct node *LeafNodeIndex[MAXN + 1];

int construct_interval_tree(struct node *parent, int val[], int lindex, int rindex)
{
    parent->lindex = lindex;
    parent->rindex = rindex;
    if (rindex == lindex)
    {
        parent->lchild = NULL;
        parent->rchild = NULL;
        parent->max = val[lindex];
        // make index for leaf node
        LeafNodeIndex[lindex] = parent;
    }
    else
    {
        int middle = (lindex + rindex) / 2;
        parent->lchild = malloc(sizeof(struct node));
        parent->lchild->parent = parent;
        parent->rchild = malloc(sizeof(struct node));
        parent->rchild->parent = parent;
        int lmax = construct_interval_tree(parent->lchild, val, lindex, middle);
        int rmax = construct_interval_tree(parent->rchild, val, middle + 1, rindex);
        parent->max = (lmax > rmax) ? lmax : rmax;
    }
    return parent->max;
}

void destroy_interval_tree(struct node *root)
{
    if (root != NULL)
    {
        destroy_interval_tree(root->lchild);
        destroy_interval_tree(root->rchild);
        free(root);
    }
}

int query_interval_tree(struct node *node, int lindex, int rindex)
{
    if (node->lindex == lindex && node->rindex == rindex)
    {
        return node->max;
    }
    else
    {
        if (rindex <= node->lchild->rindex)
        {
            return query_interval_tree(node->lchild, lindex, rindex);
        }
        else if (lindex >= node->rchild->lindex)
        {
            return query_interval_tree(node->rchild, lindex, rindex);
        }
        else
        {
            int lmax = query_interval_tree(node->lchild, lindex, node->lchild->rindex);
            int rmax = query_interval_tree(node->rchild, node->rchild->lindex, rindex);
            return (lmax > rmax) ? lmax : rmax;
        }
    }
}

void update_interval_tree(struct node *child)
{
    // if child has parent, the parent node must have 2 valid child
    if (child->parent != NULL)
    {
        int lmax = child->parent->lchild->max;
        int rmax = child->parent->rchild->max;
        int max = (lmax > rmax) ? lmax : rmax;
        if (max != child->parent->max)
        {
            // we must keep on spreading this changing upforward
            child->parent->max = max;
            update_interval_tree(child->parent);
        }
    }
}
int main()
{
    int n, m;
    int i;
    int maxScore;
    int a, b;
    char command[4];
    while(scanf("%d%d", &n, &m) == 2)
    {
        for(i = 1; i <= n; i++)
        {
            scanf("%d", ScoreTable + i);
        }
        // build interval tree
        struct node *root = malloc(sizeof(struct node));
        root->parent = NULL;
        construct_interval_tree(root, ScoreTable, 1, n);
        // build done
        for(i = 0; i < m; i++)
        {
            scanf("%s%d%d", command, &a, &b);
            if(command[0] == 'Q')
            {
                maxScore = query_interval_tree(root, a, b);
                printf("%d\n", maxScore);
            }
            else/* 'U' */
            {
                struct node *child = LeafNodeIndex[a];
                if (child->max != b)
                {
                    child->max = b;
                    update_interval_tree(child);
                }
            }
        }
        // free this interval tree
        destroy_interval_tree(root);
        root = NULL;
    }
    return 0;
}


代码是完全按着上面所述的思路来写的。遗憾的是该代码在OJ上跑的结果依然是Time Limit Exceeded!
那么按照ACM的套路,一般都是开全局数组,而不是用malloc/free。考虑到construct_interval_tree整个过程只是简单地逐个节点地分配节点,更新与查询时树结构本身并没有变化。于是可以用节点池来优化掉malloc/free。加上construct_interval_tree的val参数可以省去,直接引用全局变量,减少函数调用时的出入栈开销。
优化后的代码终于Accepted了,如下:

#include <stdio.h>
#include <stdlib.h>
#define MAXN 200000
#define MAXM 5000
/*
 * 优化:1. 放弃malloc与free,自己管理节点内存池——MemPool。malloc操作简化为MemPool + MemPoolCount++;destroy_interval_tree操作简化为直接重置MemPoolCount为0; 2. 省去construct_interval_tree的val参数,直接使用全局ScoreTable变量;
 * 优化结果: 从TLE变成1078MS,内存使用量从14340K变成11176K;
 */

struct node
{
    int max;
    int lindex;
    int rindex;
    struct node *lchild;
    struct node *rchild;
    struct node *parent;
};

int ScoreTable[MAXN + 1];
struct node *LeafNodeIndex[MAXN + 1];
struct node MemPool[MAXN * 3];
int MemPoolCount;

int construct_interval_tree(struct node *parent, int lindex, int rindex)
{
    parent->lindex = lindex;
    parent->rindex = rindex;
    if (rindex == lindex)
    {
        parent->lchild = NULL;
        parent->rchild = NULL;
        parent->max = ScoreTable[lindex];
        /* make index for leaf node */
        LeafNodeIndex[lindex] = parent;
    }
    else
    {
        int middle = (lindex + rindex) / 2;
        parent->lchild = MemPool + MemPoolCount++;
        parent->lchild->parent = parent;
        parent->rchild = MemPool + MemPoolCount++;
        parent->rchild->parent = parent;
        int lmax = construct_interval_tree(parent->lchild, lindex, middle);
        int rmax = construct_interval_tree(parent->rchild, middle + 1, rindex);
        parent->max = (lmax > rmax) ? lmax : rmax;
    }
    return parent->max;
}

void destroy_interval_tree(void)
{
    MemPoolCount = 0;
}

int query_interval_tree(struct node *node, int lindex, int rindex)
{
    if (node->lindex == lindex && node->rindex == rindex)
    {
        return node->max;
    }
    else
    {
        if (rindex <= node->lchild->rindex)
        {
            return query_interval_tree(node->lchild, lindex, rindex);
        }
        else if (lindex >= node->rchild->lindex)
        {
            return query_interval_tree(node->rchild, lindex, rindex);
        }
        else
        {
            int lmax = query_interval_tree(node->lchild, lindex, node->lchild->rindex);
            int rmax = query_interval_tree(node->rchild, node->rchild->lindex, rindex);
            return (lmax > rmax) ? lmax : rmax;
        }
    }
}

void update_interval_tree(struct node *child)
{
    /* if child has parent, the parent node must have 2 valid child */
    if (child->parent != NULL)
    {
        int lmax = child->parent->lchild->max;
        int rmax = child->parent->rchild->max;
        int max = (lmax > rmax) ? lmax : rmax;
        if (max != child->parent->max)
        {
            /* we must keep on spreading this changing upforward */
            child->parent->max = max;
            update_interval_tree(child->parent);
        }
    }
}
int main()
{
	int n, m;
	int i;
	int maxScore;
	int a, b;
	char command[4];
	while(scanf("%d%d", &n, &m) == 2)
	{
		for(i = 1; i <= n; i++)
		{
			scanf("%d", ScoreTable + i);
		}
		/* build interval tree */
		MemPoolCount = 0;
		struct node *root = MemPool + MemPoolCount++;
		root->parent = NULL;
		construct_interval_tree(root, 1, n);
		/* build done */
		for(i = 0; i < m; i++)
		{
			scanf("%s%d%d", command, &a, &b);
			if(command[0] == 'Q')
			{
				maxScore = query_interval_tree(root, a, b);
				printf("%d\n", maxScore);
			}
			else/* 'U' */
			{
				struct node *child = LeafNodeIndex[a];
				if (child->max != b)
				{
					child->max = b;
					update_interval_tree(child);
				}
			}
		}
		/* free this interval tree */
		destroy_interval_tree();
		root = NULL;
	}
	return 0;
}


考虑到二叉树里面有个完全二叉树的东东,可以实现在父、子节点在一个特殊数组(堆)里的直接下标计算由父节点得出子节点或者反回来从子节点计算出父节点。这样的话,节点本身就不用像这样子定义啦:
struct node
{
	int max;
	int lindex;
	int rindex;
	struct node *lchild;
	struct node *rchild;
	struct node *parent;
};

而是:
struct node
{
	int max;
	int lindex;
	int rindex;
};

加上父子节点在堆中的下标关系运算:
#define ROOT_INDEX 1
#define PARENT_INDEX(child) (child >> 1)
#define LCHILD_INDEX(parent) (parent << 1)
#define RCHILD_INDEX(parent) ((parent << 1) + 1)

相当的高效!

问题在于我这样子构造的二叉树是完全二叉树吗?我不知道该怎么证明。但是我却已经找到了一个反例,说明这样子构造的不全是完全二叉树,这个反例就是区间[1,14]。

在最后一层即第五层节点[6,6]与[8,8]之间的空缺了[7,7],该节点在第四层那里。

这让人相当的失望,竟然不能用堆来优化。后来仔细一想,即使不是完全二叉树依然可以利用这个堆。空缺的那个位子对于堆来说并无大碍。整棵树各节点依然以满足父子节点的下标运算关系方式分布于堆中。只不过有空隙罢了!

空隙越多,利用率越差罢了。只要我这样子构造的树不是呈线性的链式结构一路下来就行(这意味着下标每次乘以二,很快就爆掉了)!

因此利用二叉堆的代码如下:
#include <stdio.h>
#include <stdlib.h>
#define MAXN 200000
#define MAXM 5000
/*
 * 尝试:
 * 改用二叉堆,这样可以省去3个指针域。问题在于求子节点与求父节点不能再像有指针时那样直接访了,需要下标计算,仅管取半与加倍可以用右移和左移来加速,但是计算的多了,不见得一定比带指针域的方式快。
 */
/*
 * 尝试结果:时间1078MS,没变;内存使用量从11176K降到7932K;
 */

struct node
{
    int max;
    int lindex;
    int rindex;
};

int ScoreTable[MAXN + 1];
int LeafNodeIndex[MAXN + 1];
struct node MemPool[MAXN * 3];

/* 从下标1开始使用,下标0空着不使用。于是我们有:
 * parent_index = child_index / 2;
 * left_child_index = parent_index * 2;
 * right_child_index = parent_index * 2 + 1;
 */
#define ROOT_INDEX 1
#define PARENT_INDEX(child) (child >> 1)
#define LCHILD_INDEX(parent) (parent << 1)
#define RCHILD_INDEX(parent) ((parent << 1) + 1)

int construct_interval_tree(int parent, int lindex, int rindex)
{
    MemPool[parent].lindex = lindex;
    MemPool[parent].rindex = rindex;
    if (rindex == lindex)
    {
        MemPool[parent].max = ScoreTable[lindex];
        /* make index for leaf node */
        LeafNodeIndex[lindex] = parent;
    }
    else
    {
        int middle = (lindex + rindex) / 2;
        int lmax = construct_interval_tree(LCHILD_INDEX(parent), lindex, middle);
        int rmax = construct_interval_tree(RCHILD_INDEX(parent), middle + 1, rindex);
        MemPool[parent].max = (lmax > rmax) ? lmax : rmax;
    }
    return MemPool[parent].max;
}

void destroy_interval_tree(void)
{
    /* do nothing */
}

int query_interval_tree(int node, int lindex, int rindex)
{
    if (MemPool[node].lindex == lindex && MemPool[node].rindex == rindex)
    {
        return MemPool[node].max;
    }
    else
    {
        if (rindex <= MemPool[LCHILD_INDEX(node)].rindex)
        {
            return query_interval_tree(LCHILD_INDEX(node), lindex, rindex);
        }
        else if (lindex >= MemPool[RCHILD_INDEX(node)].lindex)
        {
            return query_interval_tree(RCHILD_INDEX(node), lindex, rindex);
        }
        else
        {
            int lmax = query_interval_tree(LCHILD_INDEX(node), lindex, MemPool[LCHILD_INDEX(node)].rindex);
            int rmax = query_interval_tree(RCHILD_INDEX(node), MemPool[RCHILD_INDEX(node)].lindex, rindex);
            return (lmax > rmax) ? lmax : rmax;
        }
    }
}

void update_interval_tree(int child)
{
    /* if child has parent, the parent node must have 2 valid child */
    if (child > ROOT_INDEX)
    {
        int parent = child >> 1;
        int lmax = MemPool[LCHILD_INDEX(parent)].max;
        int rmax = MemPool[RCHILD_INDEX(parent)].max;
        int max = (lmax > rmax) ? lmax : rmax;
        if (max != MemPool[parent].max)
        {
            /* we must keep on spreading this changing upforward */
            MemPool[parent].max = max;
            update_interval_tree(parent);
        }
    }
}
int main()
{
    int n, m;
    int i;
    int maxScore;
    int a, b;
    char command[4];
    while(scanf("%d%d", &n, &m) == 2)
    {
        for(i = 1; i <= n; i++)
        {
            scanf("%d", ScoreTable + i);
        }
        /* build interval tree */
        construct_interval_tree(ROOT_INDEX, 1, n);
        /* build done */
        for(i = 0; i < m; i++)
        {
            scanf("%s%d%d", command, &a, &b);
            if(command[0] == 'Q')
            {
                maxScore = query_interval_tree(ROOT_INDEX, a, b);
                printf("%d\n", maxScore);
            }
            else/* 'U' */
            {
                int child = LeafNodeIndex[a];
                if (MemPool[child].max != b)
                {
                    MemPool[child].max = b;
                    update_interval_tree(child);
                }
            }
        }
        /* free this interval tree */
        destroy_interval_tree();
    }
    return 0;
}

提交之后的运行结果在时间消耗上没变化,依然是1078ms;由于节点本身变小了,所以内存消耗变小了,为7932KB。

仅管题目Accepted了。但是这样子构造出的树的最大深度是多少?树的深度决定了二叉堆解法的内存空间消耗到底有多少。很容易看出,更新操作的时间复杂度变成了与树深度相关。那么查询操作呢?查询过程中区间被不断地拆分成小区间,越早匹配上就越省时间。当原始查询区间很大时,很容易在树的上层匹配上许多块,余下的小块零碎区间会落于下层才匹配上;当原始查询区间很小时,比如长度为1,那么一路找下来,也只需找树的深度层。

谁能给我一个定量化的时间与空间估计,以题目中的关于N和M的函数来给出?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值