题目描述
小明最近在研究压缩算法。
他知道,压缩的时候如果能够使得数值很小,就能通过熵编码得到较高的压缩比。
然而,要使数值很小是一个挑战。
最近,小明需要压缩一些正整数的序列,这些序列的特点是,后面出现的数字很大可能是刚出现过不久的数字。对于这种特殊的序列,小明准备对序列做一个变换来减小数字的值。
变换的过程如下:
从左到右枚举序列,每枚举到一个数字,如果这个数字没有出现过,刚将数字变换成它的相反数,如果数字出现过,则看它在原序列中最后的一次出现后面(且在当前数前面)出现了几种数字,用这个种类数替换原来的数字。
比如,序列(a1,a2,a3,a4,a5)=(1,2,2,1,2)在变换过程为:
a1:1未出现过,所以a1变为-1;
a2:2未出现过,所以a2变为-2;
a3:2出现过,最后一次为原序列的a2,在a2后,a3前有0种数字,所以a3变为0;
a4:1出现过,最后一次为原序列的a1,在a1后,a4前有1种数字,所以a4变为1;
a5:2出现过,最后一次为原序列的a3,在a3后,a5前有1种数字,所以a5变为1.
现在,给出原序列,请问,按这种变换规则变换后的序列是什么。
输入格式:
输入第一行包含一个整数N,序列表示的长度
第二行所有游戏N个正整数,表示输入序列。
输出格式:
输出一行,包含n个数,表示变换后的序列。
例如,输入:
5
1 2 2 1 2
程序应该输出:
-1 -2 0 1 1
再例如,输入:
12
1 1 2 3 2 3 1 2 2 2 3 1
程序应该输出:
-1 0 -2 -3 1 1 2 2 0 0 2 2
数据规模与约定
对于30%的数据,n <= 1000;
对于50%的数据,n <= 30000;
对于100%的数据,1 <= n <= 100000,1 <= ai <= 10 ^ 9
资源约定:
峰值内存消耗(含虚拟机)<256M
CPU消耗<3000ms
java代码如下
对于每一个数字,判断是否第一次遇到,如果是第一次遇到,将其加入map中,map的value为该数最后一次遇到的下标;如果不是第一次遇到,统计上一次遇到到该次遇到数字中间有多少种不同的数字。
import java.util.*;
public class Main {
static Map<Integer, Integer> map = new HashMap<Integer, Integer>();
static int N;
static int[] arr;
static int[] ans;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt();
arr = new int[N];
ans = new int[N];
for (int i = 0; i < arr.length; i++) {
arr[i] = sc.nextInt();
// 如果是第一次发现该数
if (map.get(arr[i]) == null) {
ans[i] = -arr[i];
}
// 如果不是第一次发现该数
else {
// 根据最后一次发现该数的下标,统计到当前下标出现数字的种类
// 使用set防止重复数字,可以统计有多少种不同的数
Set<Integer> set = new HashSet<Integer>();
for (int j = map.get(arr[i]) + 1; j < i; j++) {
set.add(arr[j]);
}
ans[i] = set.size();
}
// 无论该key值是否第一次出现,将其put入map中
map.put(arr[i], i);
}
for (int i = 0; i < ans.length; i++) {
System.out.print(ans[i] + " ");
}
sc.close();
}
}
代码优化
如上代码中,如果不是第一次发现某数,则需要将从上一次出现到该次出现所有的数进行遍历,时间复杂度为n。
能不能将此处优化为O(log n)呢?
这里用到了区间树的概念。
import java.util.*;
public class Main {
static Map<Integer, Integer> map = new HashMap<Integer, Integer>();
static int N;
static int[] a; // 记录原始数据
static int[] ans; // 记录变换后的结果
static int[] b; // 这是一个0,1序列,b[i]为1当且仅当a[i]代表的数字最后一次出现的位置为i
// 最后求区间和的时候只要将b对应下标的值加起来即可
static SegTree root; // 区间树
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt();
a = new int[N];
b = new int[N];
ans = new int[N];
root = buildSegTree(0, N - 1); // 构造区间树
for (int i = 0; i < a.length; i++) {
int num = sc.nextInt();
a[i] = num; // 将原数据保存起来
Integer preIndex = map.get(num);
// 如果是第一次发现该数
if (preIndex == null) {
ans[i] = -num;
b[i] = 1;
}
// 如果不是第一次发现该数
else {
// 计算map.get(a[i])+1到i-1之间不同数字的个数==>求区间和
ans[i] = query(root, preIndex + 1, i - 1);
b[preIndex] = 0; // 更新上次出现位置为0
b[i] = 1; // 更新本次出现位置为1
// 更新之前位置的区间树
update(root, preIndex, -1);
}
// 无论该key值是否第一次出现,将其put入map中
map.put(a[i], i);
// 更新i位置的区间树
update(root, i, 1);
}
for (int i = 0; i < ans.length; i++) {
System.out.print(ans[i] + " ");
}
sc.close();
}
// 计算b[p1]到b[p2]的和,通过区间树可以将其优化为log n的时间复杂度
static int query(SegTree tree, int p1, int p2) {
int l = tree.l;
int r = tree.r;
if (p1 <= l && p2 >= r) {
return tree.sum;
}
if (p1 > p2) {
return 0;
}
int mid = (l + r) / 2;
int res = 0;
if (p1 <= mid) {
res += query(tree.lSon, p1, p2);
}
if (p2 > mid) {
res += query(tree.rSon, p1, p2);
}
return res;
}
/**
* b发生变化后,需要更新区间树
*
* @param p
* 改变的b的下标
* @param num
* 增量
*/
static void update(SegTree tree, int p, int num) {
// 递归出口
if (tree == null) {
return;
}
// 处理的当前SegTree
tree.sum += num;
// 递归处理子问题
int l = tree.l;
int r = tree.r;
int mid = (l + r) >> 1;
if (p <= mid) { // 左区间应该改变
update(tree.lSon, p, num);
} else {
update(tree.rSon, p, num);
}
}
// 区间树的数据结构
static class SegTree {
int l, r; // 左右区间的下标
int sum; // 区间内的和
SegTree lSon; // 左子树
SegTree rSon;// 右子树
public SegTree(int l, int r) {
this.l = l;
this.r = r;
}
}
// 构建区间树
static SegTree buildSegTree(int l, int r) {
SegTree segTree = new SegTree(l, r);
// 左右区间相同,叶子结点,sum为b[l]
if (l == r) {
segTree.sum = b[l];
return segTree;
}
// 左右区间不同
int mid = (l + r) / 2;
SegTree lson = buildSegTree(l, mid);
SegTree rson = buildSegTree(mid + 1, r);
segTree.lSon = lson;
segTree.rSon = rson;
segTree.sum = lson.sum + rson.sum;
return segTree;
}
}