// http: // poj.org/problem?id=2104 //#include #include #include #include #include #include #include #include #include #include const int N = 100010; using namespace std; //建立一颗权值线段树,每个点存储的信息为该值域区间存在的数的个数 struct Node { int l, r; //分别表示左右节点编号 int cnt; //区间内数字个数 } tr[N * 4 + N * 17]; //初始骨架N * 4 再加上最多M次插入一共修改MlogM个节点 //也见过有的大佬说,直接开N*32就行 vector nums; //用于离散化的数组,由于数的范围很大,但是不能开那么大的空间,所以需要离散化 int root[N], idx; // root记录根节点的版本号【root[r]表示插入了第r个数版本的线段树】, idx记录树节点的版本号 int a[N]; //原始数组 int n, m; //离散化二分查找 int find(int x) { return lower_bound(nums.begin(), nums.end(), x) - nums.begin(); } //构建新版本的线段树,返回新创建的节点号 //注意里与普通版本的线段树不一样,普通线段树是的左儿子编号是父亲的编号*2,右儿子的编号是父亲的编号*2+1 //而主席树的节点编号是通过tot++随用随算的,因为多个版本,不能沿用原来的处理办法,那样就空着内存节点的太多了 int build(int l, int r) { // l,r是指数据管辖区间 int p = ++idx; //生成节点号 if (l == r) return p; //到叶子节点,直接返回新产生的节点号,不再递归 int mid = (l + r) >> 1; // 2x,2x+1,静态线段树构建办法 tr[p].l = build(l, mid); //构建左子树 tr[p].r = build(mid + 1, r); //构建右子树 return p; } /** * @brief 对原始数据,通过一个一个的insert方式存入主席树,以达到生成每个版本线段树的目标 * * @param p 上一个版本的版本号 * @param l * @param r * @param x * @return int */ int insert(int p, int l, int r, int x) { int q = ++idx; // 1、获取到可用节点编号 tr[q] = tr[p]; // 2、旧版本啥样我啥样 if (l == r) { // 3、第x大位置的数字个数增加了1个 tr[q].cnt++; return q; } int mid = (l + r) >> 1; if (x <= mid) // 4、x小于等于mid,向左半部递归 tr[q].l = insert(tr[p].l, l, mid, x); else // 5、x大于mid,向右半部递归 tr[q].r = insert(tr[p].r, mid + 1, r, x); //更新区间内数字个数,相当于pushup tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt; return q; //返回新创建的节点编号 } /** * @brief 在q这个版本的线段树中,它的前序版本是p,管辖范围是[l,r],求第k小数 * * @param q 当前版本号 * @param p 前序版本号 * @param l 左边界 * @param r 右边界 * @param k 第k小 * @return int 是什么数字 */ int query(int q, int p, int l, int r, int k) { if (l == r) return r; //到叶子节点了,返回是第l个数字,nums返回原始数字 //当前版本左儿子区间内的数字个数,减去,前序版本左儿子区间内的数字个数, //描述了在从l到r这两个版本间增加的数字数量,类似于前缀和思想 int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt; int mid = (l + r) >> 1; if (k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k); else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt); } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; nums.push_back(a[i]); } //离散化 为什么要离散化?是因为值域范围1e9,太大 sort(nums.begin(), nums.end()); nums.erase(unique(nums.begin(), nums.end()), nums.end()); //构建版本0,只有版本0需要构建 //因为离散化后的范围区间是0~nums.size()-1 //看来构建线段树的l,r其实是指的原始数组的索引位置 root[0] = build(0, nums.size() - 1); //构建不同版本的线段树 for (int i = 1; i <= n; i++) // 1、每个新版本,都只能在上一个版本基础上进行修改获取到 // 2、现在这个新版本,也和上一个版本的范围是一样的,也是[0,nums.size()-1] // 3、find(a[i]):通过二分获取a[i]这个数据现在离散化后所在的位置 root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i])); while (m--) { int l, r, k; cin >> l >> r >> k; //查询[l,r]区间中的第k大数,利用前缀和的思想,从[l ~ r]中的数据剔除掉[1 ~ l-1]的数据, //不同版本的线段树的结构都是一样的,所以初始都是从0到nums.size()这个范围当中找 printf("%d\n", nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)]); } return 0; }