将数组清空
给你一个包含若干互不相同整数的数组nums,你需要执行以下操作直到数组为空 :
- 如果数组中第一个元素是当前数组中的最小值则删除它
- 否则,将第一个元素移动到数组的末尾
请你返回需要多少个操作使nums为空
一. 数学思维减小问题规模
除去删除元素的操作外,考虑每个数需要移动的次数
假设数组按升序排列,则不需要移动
以123765489为例
通过三个删除操作变成765489
对于下一个要删除的4来说,4是不需要任何移动的,因为它与前面删除的3是升序关系
而对于要删除的5来说,因为它与4逆序,必然有一次后移操作
在删除4的时候变成89765,这时765皆后移一次,删除5的时候,存在中间状态76589,此时89也后移一次
可以发现当存在逆序的时候,所有数据都会在发生一次移动后达到原来相对位置的中间状态
相对位置便不会因为移动而发生变化,从而为下一次处理做铺垫
当前待删除数出现在了已删除数的前面(逆序)
在删除这对逆序对前一个元素的过程中,会让其他元素移动一次,恢复到原来的相对位置状态
class Solution {
public:
long long countOperationsToEmptyArray(vector<int>& nums) {
map<int, int> m;
for(int i = 0; i < nums.size(); i++)
m[nums[i]] = i;//记录值对应下标
int N = 1;//删除元素一次操作
long long res = 0;
auto it = m.begin();//最小值
res = res + N;
int index = it->second;//记录之前位置
it++;
while(it != m.end()){//从小到大遍历所有数
if(it->second<index)//比之前坐标小(逆序)
N++;//所有元素操作次数加一,恢复到原来的相对位置状态
res = res + N;
index = it->second;
it++;
}
return res;
}
};
二. 树状数组
想象成用一个下标反复遍历数组,考虑下标的总共移动次数
关键在于寻找下一个最小值位置,以及跳过途中已删除的元素(不记次数)
前者可以用排序或红黑树,后者用树状数组或线段树
class Solution {
public:
vector<int> tree;//树状数组
int n;
long long countOperationsToEmptyArray(vector<int> &nums) {
n = nums.size();
map<int, int> m;
for(int i = 0; i < nums.size(); i++)
m[nums[i]] = i;//记录值对应下标
long long res = n; // 先把 n 计入答案
tree.resize(n+1);//为了统一操作防止越界,特别是查询操作,多分配一个空间
int pre = 1; // 上一个最小值的位置,初始为 1
auto it = m.begin();
for (int k = 0; k < n; k++,it++) {//顺序访问当前最小值
int i = it->second + 1; // 下标从 1 开始
if (i >= pre) // 从 pre 移动到 i,跳过已经删除的数
res += i - pre - query(pre, i);
else // 从 pre 移动到 n,再从 1 移动到 i,跳过已经删除的数
res += n - pre + i - k + query(i, pre - 1);
updata(i,1); // 删除该数,使得已删数加一
pre = i;
}
return res;
}
int lowbit(int x){//求二进制化最后一位的值
return x&(-x);
}
void updata(int i,int k){ //在i位置加上k,O(logn)复杂度单点修改
while(i<=n){//更新子树上所有值
tree[i]+=k;
i+=lowbit(i);//移动到父亲节点
}
}
long long sum(int i){ //求数组前i项的和
long long res=0;
while(i>0){//O(logn)求前缀和
res+=tree[i];
i-=lowbit(i);//移动到前一棵子树(子区间)
}
return res;
}
long long query(int left, int right) {
return sum(right) - sum(left - 1);
}
};