树状数组相关题目

本文介绍了两种方法解决计算数组中逆序对和递增子序列问题:归并分治算法和基于树状数组的解决方案。归并分治方法空间效率高但不支持实时查询,而树状数组虽然需要离散化但可以实现区间查询。
摘要由CSDN通过智能技术生成

题目一

方法一

归并分治

代码:

# include <stdio.h>

int arr[100];
int help[100];

int n;

//归并分治
// 1.统计i、j来自 l~r 范围的情况下,逆序对数量
// 2.统计完成后,让arr[l...r]变成有序的 
int f(int l, int r)
{
	if (l == r)
		return 0;
	int m = (l + r) / 2;
	return f(l, m) + f(m+1, r) + merge(l, m, r);
}

int merge(int l, int m, int r)
{
	//i 来自 l...m
	//r 来自 m+1....r
	//统计有多少逆序对 
	int ans = 0;
	int j = r;
	for (int i=m; i>=l; --i)
	{
		while (j >= m+1 && arr[i] <= arr[j])
			j = j - 1;
		ans = ans + j - m;
	}
	
	//左右部分合并,整体变有序,归并排序的过程 
	int k = l;
	int a = l;
	int b = m + 1;
	while (a <= m && b <= r)
	{
		if (arr[a] <= arr[b])
		{
			help[k] = arr[a];
			a = a + 1;
		}
		else
		{
			help[k] = arr[b];
			b = b + 1;
		}
		k = k + 1;
	}
	while (a <= m)
	{
		help[k] = arr[a];
		k = k + 1;
		a = a + 1;
	}
	while (b <= m)
	{
		help[k] = arr[b];
		k = k + 1;
		b = b + 1;
	}
	for (i = l; i <= r; ++i)
		arr[i] = help[i];
	return ans; 
}

int main()
{
	scanf("%d", &n);
	for (int i=0; i<n; ++i)
		scanf("%d", &arr[i]);
	
	int ans = f(0, n-1);
}

方法二

树状数组

我们假设一个数组arr

建立一个词频数组

用于记录数字出现的次数

因为arr数组中最大的数字为100

所以只要建立一个长度为100的词频数组即可

我们对原数组从右往左进行考虑

先考虑arr[8](也就是2)

对于逆序对,i  <  j,并且arr[i] > arr[j]

因此我们只要统计小于2的数,在词频数组中的和即可

也就是词频数组中下标2之前的前缀和

在统计完sum后,再将2添加进词频数组中

这时,我们就可以把词频数组转为树状数组

以上的操作就变为了

单点修改和区间查询

注意:

此时要考虑树状数组长度的问题!!!

因为词频数组的长度是由原数组最大值决定的,可能会出现过大的情况

此时就要进行离散化处理

我们可以假设另外一个数组help

将原数组按照从小到大的顺序填入help数组中

并进行去重(可以使用双指针)

然后通过help数组对arr数组进行修改

利用二分

找到原数组中的数字在help数组中的下标位置

并将该数字修改为help数组中的下标的值

  

再对此时的arr数组进行词频统计建立树状数组

代码:

# include <stdio.h>
# include <stdlib.h>
int arr[100];
int sort[100];
int tree[100];

int n, m;

int cmp(const void* a, const void* b)
{
	int p1 = *(int*)a;
	int p2 = *(int*)b;
	return p1 - p2;
}

int lowbit(int x)
{
	return x & (-x);
}

void add(int i, int v)
{
	while (i <= m)
	{
		tree[i] = tree[i] + m;
		i = i + lowbit(x);
	}
}

int sum(int i)
{
	int ans = 0;
	while (i > 0)
	{
		ans = ans + tree[i];
		i = i - lowbit(i;)
	}
	return ans;
}


// 给定原始值v
// 返回sort数组中1~m的下标 
int rank(int v) 
{
	int l = 1;
	int r = m;
	int mid;
	int ans = 0;
	while (l<=r)
	{
		mid = (l + r) / 2;
		if (sort[mid] >= v)
		{
			ans = mid;
			r = mid - 1;
		}
		else
		{
			l = mid + 1;
		}
	}
	return ans;
}

int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i)
	{
		scanf("%d", arr[i]);
		sort[i] = arr[i];
	}
	qsort(sort, sizeof(int), n, cmp);
	m = 1;
	for (int i=2; i <= n; ++i)
	{
		if (sort[i] != sort[m])
		{
			m = m + 1;
			sort[m] = sort[i];
		}
	}
	int ans = 0;
	for (int i=1; i<=n; ++i)
		arr[i] = rank(arr[i]);
	
	for (int i=n; i>=1; --i)
	{
		//增加当前数字的词频 
		add(arr[i], 1);
		// 右边有多少数字是 <= 当前数值 - 1 
		ans = ans + sum(arr[i]-1);
	}
	printf("%d", ans);
}

两个方法的区别:

归并分治实现:无需离散化代码、使用空间少、常数时间优良,不能实时查询,只能是离线的批量过程

树状数组实现:需要离散化代码、使用空间多、常数时间稍慢,可以实时查询

题目二(典型)

我们建立两个数组up1, up2

up1[ i ]:以下标 i 做结尾的升序的只有一个元素的数量

up2[ i ]:以下标 i 做结尾的升序的含有两个元素的数量

假设数组arr为:

从左往右进行处理

我们加入第一个数字为arr[1] = 1

先判断以 1 做结尾的升序的含有三个元素的数量

我们发现该值就是在up2数组中小于1的前缀和

然后再更新以 1 做结尾的升序的含有两个个元素的数量(也就是up2[1])

我们发现该值就是在up1数组中小于1的前缀和

找到该值后,填入up2[1]

更新以 1 做结尾的升序的含有一个元素的数量(也就是up1[1])

依次对 arr 数组从左往右进行遍历

因此,我们先对数组中的元素进行离散化处理

然后从左往后按照上述步骤进行

代码:

# include <stdio.h>
# include <string.h>
//记录原数组 
int arr[100];

//建立去重离散化数组 
int sort[100];

//维护信息:up1
//tree1不是up1数组,是up1数组的树状数组 
int tree1[100]; 

//维护信息:up2
//tree2不是up2数组,是up2数组的树状数组 
int tree2[100];

int n, m;

int cmp(const void* a, const void* b)
{
    int* p1 = (int*)a;
    int* p2 = (int*)b;
    return *p1 - *p2;
}

int lowbit(int x)
{
	return x & (-x);
}

void add(int* tree, int i, int v)
{
	while (i <= m)
	{
		tree[i] = tree[i] + v;
		i = i + lowbit(i);
	}
}

int sum(int* tree, int i)
{
	int ans = 0;
	while (i > 0)
	{
		ans = ans + tree[i];
		i = i - lowbit(i);
	}
	return ans;
}

int rank(int x)
{
	int l = 1;
	int r = m;
	int mid;
	int ans = 0;
	while (l <= r)
	{
		mid = (l + r) / 2;
		if (sort[mid] >= v)
		{
			ans = mid;
			r = mid - 1;
		}
		else	
			l = mid + 1;
	}
	return ans;
}

int main()
{
	scanf("%d", &n);
	for (int i=1; i<=n; ++i)
	{
		scanf("%d", &arr[i]);
		sort[i] = arr[i];
	}
    qsort(sort, sizeof(int), n, cmp);
	m = 1;
	for (int i=2; i<=n; ++i)
	{
		if (sort[m] != sort[i])
		{
			m = m + 1;
			sort[m] = sort[i];
		}
	}
	for (int i=1; i<=n; ++i)
		arr[i] = rank(arr[i]);
	
	
	int ans = 0;
	for (int i=1; i<=n; ++i)
	{
		ans = ans + sum(tree2, arr[i] - 1);
		add(tree1, arr[i], 1);
		add(tree2, arr[i], sum(tree2, arr[i] - 1));
	}	
	printf("%d", ans);
}

题目三

我们建立一个结构

用于存储以下标 i 为结尾的子序列的最大长度和个数

# include <stdio.h>

struct node
{
	int len; //长度 
	int num; //个数 
};

//不是该结构数组,是该结构体的树状数组
//用于维护一个范围的最大长度和个数
struct node tree[100];

int arr[100];
int sort[100];

int n, m;

//查询结尾数值<=i的所有递增子序列中
//长度最大的递增子序列长度是多少,赋值给maxlen和maxnum
int maxlen, maxnum;

int lowbit(int x)
{
	return x & (-x);
}

//以 i 这个值结尾的最长递增子序列达到了len长度
//并且这样的最长递增子序列的数量达到了cnt个
void add(int i, int l, int cnt)
{
	while (i <= m)
	{
		if (tree[i].len == l)
			tree[i].num = tree[i].num + cnt;
		else if (tree[i].len < l)
		{
			tree[i].len = l;
			tree[i].num = cnt;
		}
		i = i + lowbit(i);
	}
}

//结尾数值 <= i 的情况下,最长递增子序列的长度和个数为多少
void query(int x)
{
	maxlen = 0;
	maxnum = 0;
	while (x > 0)
	{
		if (tree[x].len == maxlen)
			maxnum = tree[x].num + maxnum;
		else if (tree[x].len > maxlen)
		{
			maxlen = tree[x].len;
			maxnum = tree[x].num;
		}
		x = x - lowbit(x);
	}
}

int rank(int x)
{
	int l = 1;
	int r = m;
	int mid;
	int ans;
	while (l <= r)
	{
		mid = (l+r) / 2;
		if (sort[mid] >= x)
		{
			r = mid - 1;
			ans = mid;
		}
		else
			l = mid + 1;
	}
	return ans;
}

int main()
{
	scanf("%d", &n);
	for (int i=1; i<=n; ++i)
	{
		scanf("%d", &arr[i]);
		sort[i] = arr[i];
	}
	qsort(sort, sizeof(sort), n, cmp);
	int m = 1;
	for (int i=2; i<=n; ++i)
	{
		if (sort[m] != sort[i])
		{
			sort[m] = sort[i];
			m = m + 1;
		}
	}
	memset(tree, 0, sizeof(tree));
	int y;
	for (int i=1; i<=m; ++i)
	{
		y = rank(arr[i]);
		query(y-1);
		add(y, maxlen+1, max(1, maxnum));
	}
	int ans = query(m);
	printf("%d", ans);
}

题目四

本题转化是非常巧妙的

我们首先需要对查询进行排序

按照 r 的大小进行排序

如图:

一个虚拟数组cnt[](之所以说它是虚拟数组,因为可以用树状数组实现想要的操作,不需要真的定义这么一个数组)。

map[i] 记录 i 位置上的数上一次出现的位置

当遍历到原数组的下标 i 时,cnt[i]+1,如果 i 位的这个数之前出现过,即存在map[i],则将cnt[map[i]]-1(在遍历到i之前已经遍历过last[i]位置了,因此cnt[map[i]]=1,所以这样操作下来,cnt[map[i]]=0),不会对当前要求的结果造成影响

比如序列:2,3,4,3,4,2

当右边界为5时(下标从1开始),对应的cnt[]值分别为:1,0,0,1,1

所以区间【1,5】内不同的数字个数=3,区间【2,5】内不同的数字个数=2,区间【4,5】内不同的数字个数=2(树状数组累加和求解)

因为右边界一定,所以我们必须要确保选择的1是尽可能靠近右边界,确保答案正确

# include <stdio.h>
# include <string.h>
struct node
{
	int l;
	int r;
	int k;
};

int arr[100];

struct node query[100];

int ans[100];

int map[100];

int tree[100];

int n, m;

int compare(const void*a, const void* b)
{
	struct node* p1 = (struct node*)a;
	struct node* p2 = (struct node*)b;
	return *p1.r - *p2.r;
}

int lowbit(int x)
{
	return x & (-x);
}

void add(int i, int v)
{
	while (i <= n)
	{
		tree[i] = tree[i] + v;
		i = i + lowbit(i);
	}
}

int q(int x)
{
	int ans = 0;
	while (x > 0)
	{
		ans = tree[x] + ans;
		x = x - lowbit(x);
	}
	return ans;
}

int range(int l, int r)
{
	return sum(r) - sum(l-1);
}

void cmp(void)
{
	 qsort(query, sizeof(struct node), m, compare);
	 int q = 1; //遍历询问 
	 int l;
	 int r;
	 int k; 
	 for (int q=1; q <= m; ++i)
	 {
	 	r = query[q].r;
	 	for (; s<=r; ++s)
	 	{
	 		int color = arr[s];
	 		if (map[color] != 0)
	 			add(map[color], -1)
	 		add[s, 1];
	 		map[color] = s;
		}
		l = query[q].l;
		k = query[q].k;
		ans[i] = range(l, r);
	 }
}

int main()
{
	scanf("%d", &n);
	for (int i=1; i<=n; ++i)
	{
		scanf("%d", &arr[i]);
	}
	scanf("%d", &m);
	for (int i=1; i<=m; ++i)
	{
		scanf("%d %d", &query[i].l, &query[i].r);
		query[i].k = i;
	}
	
	cmp();
	for (int i=1; i<=m; ++i)
		printf("%d", ans[i]);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值