UESTC2022暑假前集训 数据结构
知识点:并查集,ST表,块状链表,树状数组,线段树,二维线段树,(带修)主席树,(回滚)莫队,CDQ分治,Treap等。
A-猫为什么讨厌狗是有理由的 解题报告
题目大意
有个糖罐,坚固度为,个钥匙,可以打开坚固度为的糖罐。可以对糖罐进行任意次攻击,使得某个糖罐的坚固度由变为。
次询问对于下标为区间的糖罐,在打开最多糖罐的前提下,他最少需要带多少钥匙。
最多修改k张纸币的面值,问最小花费,或判定无解。
。
解题思路
事实上,我们可以对钥匙进行”攻击“。从小到大排序,对一个钥匙不断攻击至0,如果一个钥匙经过某些次”攻击“后与已有的钥匙相同,则这把钥匙是多余的。我们去除所有多余的钥匙,则所有糖罐至多被剩下的一种钥匙打开,我们记不能被任何钥匙打开的糖罐对应的钥匙为0号钥匙。
问题转化为,查询内非零的数中不同数的个数。主席树!
方法1:
注意到,我们可以将每个糖罐用1 << (x - 1)
表示,x为对应的钥匙编号,x=0则置为0,这样,区间的答案为所有数或起来,得到的数二进制中"1"的个数。
区间或运算具有可重复贡献的性质,因此可以用ST表维护静态区间或。
方法2:
注意到,如果我们对每个询问能够遍历每个钥匙查询也可以通过。
因此只需要对每个钥匙维护前缀使用次数,查询是否被使用只需看前缀和相减是否非0即可。
代码实现
//
// Created by vv123 on 2022/4/30.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 1, null = -1;
#define int long long
int n, m, d, q, cnt, a[N], k[N], key[N], f[N][20], h[N + 1];
int sum[64][N];
int find (int x) {
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x) {
k++;
if(k == N) k = 0;
}
return k;
}
inline int solve(int x) { //用第几把钥匙开
while(x) {
for (int i = 1; i <= cnt; i++)
if (key[i] == x)
return i;//1ll << i - 1;
x /= d;
}
return 0ll;
}
inline void ST() { //f[i][j] = OR of [i, i + 2^j - 1]
for (int i = 1; i <= n; i++) f[i][0] = solve(a[i]);//, cout << bitset<5>(f[i][0]) << endl;
for (int j = 1; j <= 20; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = f[i][j - 1] | f[i + (1 << (j - 1))][j - 1];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
for (int i = 1; i < N; i++) h[i] = null;
cin >> n >> m >> d >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> k[i];
}
sort(k + 1, k + 1 + m);
h[find(k[1])] = k[1]; key[++cnt] = k[1];
for (int i = 2; i <= m; i++) {
int x = k[i];
bool exist = false;
while (x) {
if (h[find(x)] != null) {
exist = true;
break;
}
x /= d;
}
if (!exist) h[find(k[i])] = k[i], key[++cnt] = k[i];
}
//至多60个数,查询区间不同数的个数
//维护每个数的前缀出现次数,对每个区间,遍历每个数的区间和是否非0
for (int i = 1; i <= n; i++) {
int t = solve(a[i]);
for (int j = 1; j <= m; j++) {
if (j == t) sum[j][i] = sum[j][i-1] + 1;
else sum[j][i] = sum[j][i - 1];
}
}
while (q--) {
int l, r;
cin >> l >> r;
int ans = 0;
for (int i = 1; i <= m; i++) {
if (sum[i][r] - sum[i][l - 1] > 0) ans++;
}
cout << ans << "\n";
}
//for (int i = 1; i <= cnt; i++) cout << key[i] << " ";
/*
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++)
cout << sum[i][j] << " ";
cout << endl;
}
ST();
while (q--) {
int l, r, s;
cin >> l >> r;
s = log2(r - l + 1);
cout << bitset<64>(f[l][s] | f[r - (1 << s)][s]).count() << "\n";
}
*/
return 0;
}
B - 啊哈哈哈,这收集糖果多是一件美事啊 解题报告
题目大意
区间加,区间乘,区间和。
。
解题思路
可以使用线段树解决。
对于区间修改,如果用暴力单点修改来解决,单次修改,不如直接写暴力,这时候需要用”延迟标记“来解决。
我们用尽量少的区间拼出修改区间,对每一个区间打上”该值已修改,但子节点未更新“的标记,这样单次修改是的。当我们访问到某个有标记的节点时,再将标记下传(pushdown操作)。
对于只有加法或只有乘法的情况,很容易做。当加法和乘法混合时如何处理呢?
对区间乘操作,我们对sum、add标记和mul标记都乘k。为啥add * = k?因为对子节点有(sum + add) * k == sum * k + add * k。配合上面的操作,在pushdown时,先下传mul标记,后下传add标记,因为add标记在父节点区间乘操作中已经被乘过了。
代码实现
//
// Created by vv123 on 2022/4/24.
//
#include <bits/stdc++.h>
#define ll long long
using namespace std;
#define getchar() (frS==frT&&(frT=(frS=frBB)+fread(frBB,1,1<<15,stdin),frS==frT)?EOF:*frS++)
char frBB[1<<15],*frS=frBB,*frT=frBB;
template<typename T>
inline void read(T &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c)){x=x*10+c-'0';c=getchar();}
}
const int N = 4e5 + 10;
const int mod = 998244353;
int n, m, a[N];
ll add[N], mul[N], sum[N];
#define mid (l+r>>1)
#define len (r-l+1)
#define llen (mid-l+1)
#define rlen (r-mid)
#define ls (i<<1)
#define rs (i<<1|1)
#define allin s <= l && r <= t //[s,t]与[l,r]一定有交集,只需考虑三种情况
#define lsin s <= mid
#define rsin t >= mid + 1
inline void pushup(int i) {
sum[i] = (sum[ls] + sum[rs]) % mod;
}
inline void pushdown(int l, int r, int i) {
if (mul[i] != 1) {
(mul[ls] *= mul[i]) %= mod; (mul[rs] *= mul[i]) %= mod;
(add[ls] *= mul[i]) %= mod; (add[rs] *= mul[i]) %= mod;
(sum[ls] *= mul[i]) %= mod; (sum[rs] *= mul[i]) %= mod;
mul[i] = 1;
}
if (add[i]) {
(sum[ls] += add[i] * llen) %= mod; (sum[rs] += add[i] * rlen) %= mod;
(add[ls] += add[i]) %= mod; (add[rs] += add[i]) %= mod;
add[i] = 0;
}
}
void Build(int l, int r, int i) {
mul[i] = 1;
if (l == r) { sum[i] = a[l]; return; }
Build(l, mid, ls); Build(mid + 1, r, rs);
pushup(i);
}
void Add(int s, int t, int l, int r, int i, int k) {
if (allin) {
(sum[i] += k * len) %= mod;
(add[i] += k) %= mod;
return;
}
pushdown(l, r, i);
if (lsin) Add(s, t, l, mid, ls, k);
if (rsin) Add(s, t, mid + 1, r, rs, k);
pushup(i);
}
void Mul(int s, int t, int l, int r, int i, int k) {
if (allin) {
(add[i] *= k) %= mod;
(mul[i] *= k) %= mod;
(sum[i] *= k) %= mod;
return;
}
pushdown(l, r, i);
if (lsin) Mul(s, t, l, mid, ls, k);
if (rsin) Mul(s, t, mid + 1, r, rs, k);
pushup(i);
}
ll Query(int s, int t, int l, int r, int i) {
ll res = 0;
if (allin) return sum[i];
pushdown(l, r, i);
if (lsin) (res += Query(s, t, l, mid, ls)) %= mod;
if (rsin) (res += Query(s, t, mid + 1, r, rs)) %= mod;
return res;
}
int main() {
read(n); read(m);
for (int i = 1; i <= n; i++) read(a[i]);
Build(1, n, 1);
while(m--) {
int op, s, t, k;
read(op); read(s); read(t);
if (op == 1) {
read(k); Mul(s, t, 1, n, 1, k);
} else if (op == 2) {
read(k); Add(s, t, 1, n, 1, k);
} else {
printf("%lld\n", Query(s, t, 1, n, 1));
}
}
return 0;
}
C-孤独的他,一个人数糖果 解题报告
题目大意
单点修改,区间第k小。
。
解题思路
作为一个数据结构小菜鸡,首先谈谈我对静态主席树的理解。
如果查询查询区间固定为,则在值域线段树(线段表示值域,结点的值表示位于这个值域的数的个数)上二分即可,过程不再赘述。
为查询任意区间,我们可以开n个版本的值域线段树,第i版本包含了a[1],a[2],...a[i]的信息,那么:
不同版本的值域线段树具有可减性。
具体来说,如果第u个版本的值域线段树上,值域[a, b]内有sumu个数;第v个版本的值域线段树上,值域[a, b]内有sumv个数。
那么显然在a[u+1],a[u+2]...a[v]这些数中,值域[a, b]内有sumv - sumu个数。
有种前缀和的既视感?没错,区间[L,R]的查询只需在”第R版值域线段树与第L-1版权值线段树的差树“上二分即可。
然而开n棵值域线段树的时空复杂度是不可接受的。但是注意到,一个新版本的值域线段树和上一版本相比,只修改一个叶子节点、总共O(log n)个节点,因此我们可以将不变的节点连到已有的节点上,只对修改的节点动态开点就可以了!这样只会增加O(nlogn)的空间开销。
这就是传说中的主席树。
下面考虑对原序列单点修改应该怎么做。
修改了第x个数,会影响到第x至n棵值域线段树,每棵值域线段树都要把旧a[x]位置-1新a[x]位置+1。这就像刚学前缀和就去做【模板】树状数组 1。。
既然值域线段树具有类似前缀和的性质,我们可以用树状数组套值域线段树。
外层维护的是区间的划分(外层的树状数组的作用是帮助我们找到一段区间对应的若干值域线段树的根节点),内层是以对应外层节点的区间内的a[i]为信息建立的值域线段树。
查询sum(L,R)时,我们从 ”在R和L-1的差树上二分“ 改为 ”在一系列信息不相交的‘加节点’的并 与 一系列信息不相交的‘减节点’的并 的差树上二分 “
修改原序列一个节点时,外层树状数组需要修改O(logN)个节点,对内层值域线段树的修改是通过新建一个版本,即如前文所述新建O(logN)个节点,然后将外层节点指向新版本的根节点,此时旧版本被丢弃。
这样,单次修改的复杂度是的。
代码实现
//
// Created by vv123 on 2022/5/1.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
struct Query {
int op, l, r, k;
} q[N];
struct Node {
int l, r, cnt;
} tr[N << 9];
int n, m, a[N], root[N], idx, siz = 32768;
#define ls tr[u].l,l,mid
#define rs tr[u].r,mid+1,r
#define mid (l + r >> 1)
void update(int& u, int l, int r, int pos, int d) {
if (!u) u = ++idx;
tr[u].cnt += d;
if (l == r) return;
if (pos <= mid) update(ls, pos, d);
else update(rs, pos, d);
}
void add(int i, int d) {
int pos = a[i];
//printf("(%d,%d)\n", i, d);
//cout << "->" << a[i] << endl;
for (; i <= n; i += i & -i)
update(root[i], 0, siz - 1, pos, d);
}
int node[2][30], cnt0, cnt1;//0:加结点 1:减结点
int query(int l, int r, int k) {
if (l == r) return l;
int cnt = 0;
for (int i = 1; i <= cnt0; i++) //统计差树左子树的cnt
cnt += tr[tr[node[0][i]].l].cnt;
for (int i = 1; i <= cnt1; i++)
cnt -= tr[tr[node[1][i]].l].cnt;
if (k <= cnt) {
for (int i = 1; i <= cnt0; i++)
node[0][i] = tr[node[0][i]].l;
for (int i = 1; i <= cnt1; i++)
node[1][i] = tr[node[1][i]].l;
return query(l, mid, k);
} else {
for (int i = 1; i <= cnt0; i++)
node[0][i] = tr[node[0][i]].r;
for (int i = 1; i <= cnt1; i++)
node[1][i] = tr[node[1][i]].r;
return query(mid + 1, r, k - cnt);
}
}
int get(int l, int r, int k) {
memset(node, 0, sizeof node);
cnt0 = cnt1 = 0;
for (int i = r; i; i -= i & -i)
node[0][++cnt0] = root[i];
for (int i = l - 1; i ; i -= i & -i)
node[1][++cnt1] = root[i];
return query(0, siz - 1, k);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
string op;
cin >> op;
if (op == "Q") {
cin >> q[i].l >> q[i].r >> q[i].k;
q[i].op = 1;
} else {
cin >> q[i].l >> q[i].r;
q[i].op = 2;
}
}
for (int i = 1; i <= n; i++)
add(i, 1);
for (int i = 1; i <= m; i++) {
if (q[i].op == 1) {
cout << get(q[i].l, q[i].r, q[i].k)<< "\n";
} else {
add(q[i].l, -1);
a[q[i].l] = q[i].r;
add(q[i].l, 1);
}
}
return 0;
}
D-辉夜大小姐希望完成她的愿望 解题报告
题目大意
插入、删除、查x的rank、查rank=x的数、求x的前驱、求x的后继。
。
解题思路
这些操作都可以用二叉搜索树(BST,Binary Search Tree)来实现。
二叉搜索树是一种二叉树,满足每个节点的权值都大于它的左儿子、小于它的右儿子,换句话说,二叉搜索树的中序遍历是升序序列。
每个节点除了权值还可以保存这个权值的出现次数、以该节点为根的子树大小。
有了这些信息,很容易实现题目中的6种操作。
这些操作所花费的时间与这棵树的高度成正比。二叉搜索树的期望高度为 。但在最坏情况下(插入一系列有序数字,成一条链)为。我们需要想办法让树高维持在 左右,即实现一个平衡二叉树。这里使用的是Treap。
简单地说,Treap是一棵拥有键值、优先级两种权值的树。对于键值而言,它是BST,对于优先级而言,它是堆(满足父亲的优先级大于儿子)。
不难证明,如果每个结点的优先级事先给定且互不相等,整棵树的形态也就唯一确定了,和元素的插入顺序无关。在Treap的插入算法中,每个节点的优先级是随机确定的,因此各个操作的时间复杂度也是随机的。幸运的是,可以证明插入、删除和查找的期望时间复杂度均为O(nlogn)。
如何在维持BST性质的前提下维护Treap的堆性质(或者其他平衡树实现平衡的机制)?
答案是通过旋转,分为zig右旋和zag左旋
我们在更新某个点p的左子节点后,如果左子节点的优先级高于p,需要将左子节点提上来,即对p执行右旋(zig)操作。
我们在更新某个点p的右子节点后,如果右子节点的优先级高于p,需要将右子节点提上来,即对p执行左旋(zag)操作。
//do sth. to update tr[p].l ... then:
if (tr[tr[p].l].val > tr[p].val) zig(p);
//do sth. to update tr[p].r ... then:
if (tr[tr[p].r].val > tr[p].val) zag(p);
zig和zag要怎么写呢?
我们只需在DNA里刻下这个图:
x ->zig y
/ \ / \
y x
/ \ / \
z <-zag z
看左边这个图,我们考虑如何在满足BST性质的前提下,把x的左子节点y提上来。
首先要保持y<x,将y的右儿子设为x。那y原来的右儿子z往哪放呢?将x的左儿子设为z。
y的左儿子和x的右儿子没变。容易验证经过以上两个操作,中序遍历不发生改变。
完成操作后,需要依次x和y的siz信息(pushup操作)。最后通过传引用把p修改为y。
inline void zig(int& x) {
int y = tr[x].l;
tr[x].l = tr[y].r, tr[y].r = x;
pushup(x), pushup(y);
x = y;
}
写出zig后,可以对称地写出zag。
还有一个麻烦的点,如何从Treap上删除一个节点p。
既然我们有旋转操作,我们可以把p递归地往下旋直到叶子,然后删除。
为了保持堆性质,如果当前左孩子优先级较大就zig(提左孩子),右儿子优先级较大就zag(提右孩子)。
(如果一个孩子为空,提另一个孩子)
void remove(int& p, int key) {
if (!p) return;
if (key == tr[p].key) {
if (tr[p].cnt > 1) tr[p].cnt--;
else { //正式删除
if (tr[p].l || tr[p].r) { //不是叶节点,旋成叶节点后删除
if (!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val)//2.删除时维护堆性质
zig(p), remove(tr[p].r, key);
else zag(p), remove(tr[p].l, key);//注意,p的值会被改变,使其保持在原来的"位置"
}
else p = 0;//是叶节点,删掉(使其祖先指向空节点)
}
}
else {
if (key < tr[p].key) remove(tr[p].l, key);
else remove(tr[p].r, key);
}
pushup(p);//任何修改操作都别忘了pushup QAQ
}
代码实现
//
// Created by vv123 on 2022/4/25.
//
#include <bits/stdc++.h>
using namespace std;
//#define getchar() (frS==frT&&(frT=(frS=frBB)+fread(frBB,1,1<<15,stdin),frS==frT)?EOF:*frS++)
//char frBB[1<<15],*frS=frBB,*frT=frBB;
template<typename T>
inline void read(T &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c)){x=x*10+c-'0';c=getchar();}
}
const int N = 1e5 + 10, INF = 0x3f3f3f3f, MAXX = 1e7;
struct node {
int l, r, key, val, cnt, size;
} tr[N];
#define ls tr[p].l
#define rs tr[p].r
int n, root, idx;
void pushup(int p) {
tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt;
}
int get_node(int key) {
tr[++idx].key = key;
tr[idx].val = rand();
tr[idx].cnt = tr[idx].size = 1;
return idx;
}
void build() {
get_node(-INF); get_node(INF);
root = 1; tr[1].r = 2;
}
/*
x ->zig y
/ \
y x
\ /
z <-zag z
*/
inline void zig(int& x) {
int y = tr[x].l;
tr[x].l = tr[y].r, tr[y].r = x;
pushup(x), pushup(y);
x = y;
}
inline void zag(int& y) {
int x = tr[y].r;
tr[y].r = tr[x].l, tr[x].l = y;
pushup(y), pushup(x);
y = x;
}
void insert(int& p, int key) {
if (!p) p = get_node(key);
else if (key == tr[p].key) tr[p].cnt++;
else if (key < tr[p].key) {
insert(tr[p].l, key);
if (tr[tr[p].l].val > tr[p].val) zig(p);//1.插入时维护堆性质
}
else if (key > tr[p].key) {
insert(tr[p].r, key);
if (tr[tr[p].r].val > tr[p].val) zag(p);
}
pushup(p);
}
void remove(int& p, int key) {
if (!p) return;
if (key == tr[p].key) {
if (tr[p].cnt > 1) tr[p].cnt--;
else { //正式删除
if (tr[p].l || tr[p].r) { //不是叶节点,旋成叶节点后删除
if (!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val)//2.删除时维护堆性质
zig(p), remove(tr[p].r, key);
else zag(p), remove(tr[p].l, key);//注意,p的值会被改变,使其保持在原来的"位置"
}
else p = 0;//是叶节点,删掉(使其祖先指向空节点)
}
}
else {
if (key < tr[p].key) remove(tr[p].l, key);
else remove(tr[p].r, key);
}
pushup(p);//任何修改操作都别忘了pushup QAQ
}
int get_rank_by_key(int p, int key) {
if (!p) return 0;
if (key == tr[p].key) return tr[tr[p].l].size + 1;
if (key < tr[p].key) return get_rank_by_key(tr[p].l, key);
return tr[tr[p].l].size + tr[p].cnt + get_rank_by_key(tr[p].r, key);
}
int get_key_by_rank(int p, int rank) {
if (!p) return INF;
if (rank <= tr[tr[p].l].size) return get_key_by_rank(tr[p].l, rank);
if (rank <= tr[tr[p].l].size + tr[p].cnt) return tr[p].key;
return get_key_by_rank(tr[p].r, rank - tr[tr[p].l].size - tr[p].cnt);
}
int get_prev(int p, int key) {
if (!p) return -INF;
if (key <= tr[p].key) return get_prev(tr[p].l, key);
return max(tr[p].key, get_prev(tr[p].r, key));//左<tr[p].key<key,无需在左子树中找
}
int get_next(int p, int key) {
if (!p) return INF;
if (key >= tr[p].key) return get_next(tr[p].r, key);
return min(tr[p].key, get_next(tr[p].l, key));//右>tr[p].key>key,无需在右子树中找
}
int main() {
build();
scanf("%d", &n);
while (n--) {
int op, x, t;
scanf("%d%d", &op, &x);
if (op == 1) insert(root, x);
else if (op == 2) remove(root, x);
else if (op == 3) printf("%d\n", get_rank_by_key(root, x) - 1);//注意树上有—INF哨兵
else if (op == 4) printf("%d\n", get_key_by_rank(root, x + 1));
else if (op == 5) (t = get_prev(root, x)) < -MAXX ? puts("NOT FOUND") : printf("%d\n", t);
else if (op == 6) (t = get_next(root, x)) > MAXX ? puts("NOT FOUND") : printf("%d\n", t);
}
return 0;
}
E-啥b二次元 解题报告
题目大意
依次向序列中插入数,求每次插入后的逆序对数。
解题思路
我们设排列中某个数由插入时间time,位置pos,值val三个属性。
则逆序对(i,j)的产生条件为:timej > timei, posj > posi, valj < vali,或者:timej > timei, posj < posi, valj > vali
这是经典的三维偏序问题,可以用CDQ分治来解决。
“归并排序求逆序对/二维偏序”的算法中已经展现了CDQ分治的基本思想:左区间的点和右区间所有点相比已经满足某种条件,对右区间的每一个点,考察左区间有多少点满足另一条件,从而统计出符合要求的点对数量。
而对于本问题(三维偏序),time是自然有序的,每次归并前左区间中点的time都小于右区间所有点。以timej > timei, posj > posi, valj < vali为例,我们按照pos从小到大进行归并(依次入队),这样我们每次新入队一个右区间节点j,前面出现的左子区间i节点都满足timej > timei, posj > posi了,只需统计有多少i满足出valj < vali即可。显然这个过程可以使用权值线段树/树状数组优化。对于另一种逆序对,可以另写一个cdq分治,通过pos从大到小入队来实现。
考虑归并排序有log n层,每层因为存在树状数组操作时间复杂度为nlog n,总的时间复杂度为
代码实现
//
// Created by vv123 on 2022/4/30.
//
//逆序对的产生条件为:timej > timei, posj > posi, valj < vali
//或者:timej > timei, posj < posi, valj > vali
//CDQ前序列按时间有序,进行归并时,右区间的timej一定大于左区间,无需考虑。只需按pos的某种顺序合并,在"入队"时统计有多少符合对应条件的vali即可
//方便起见,做两次CDQ
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, pos[N];
long long t[N], ans[N];
void add(int i, int x) {
for (; i <= n; i += i & -i)
t[i] += x;
}
long long sum(int i) {
long long res = 0;
for (; i; i -= i & -i)
res += t[i];
return res;
}
struct node {
int time, pos, val;
bool operator < (const node& x) const {
return time < x.time;
}
} a[N], b[N];
void CDQ1(int l, int r) {
if (l == r) return;
int mid = l + r >> 1, i = l, j = mid + 1, k = l - 1;
CDQ1(l, mid); CDQ1(mid + 1, r);
while (i <= mid && j <= r) {
if (a[i].pos < a[j].pos) {
add(a[i].val, 1);
b[++k] = a[i++];
} else {//考虑j前面比它大的数的贡献
ans[a[j].time] += sum(n) - sum(a[j].val);
b[++k] = a[j++];
}
}
while (i <= mid) {
add(a[i].val, 1);//没用,但是方便恢复qwq。如果memset(t, 0, sizeof t),至少第二层就有O(N)个区间,N^2肯定会T的。。
b[++k] = a[i++];
}
while (j <= r) {
ans[a[j].time] += sum(n) - sum(a[j].val);
b[++k] = a[j++];
}
for (int p = l; p <= mid; p++) add(a[p].val, -1);
for (int p = l; p <= r; p++) a[p] = b[p];
}
void CDQ2(int l, int r) {
if (l == r) return;
int mid = l + r >> 1, i = l, j = mid + 1, k = l - 1;
CDQ2(l, mid); CDQ2(mid + 1, r);
while (i <= mid && j <= r) {
if (a[i].pos > a[j].pos) {
add(a[i].val, 1);
b[++k] = a[i++];
} else {//考虑j后面比它小的数的贡献(按pos从大到小入队,即为已有的val)
ans[a[j].time] += sum(a[j].val - 1);
b[++k] = a[j++];
}
}
while (i <= mid) {
add(a[i].val, 1);
b[++k] = a[i++];
}
while (j <= r) {
ans[a[j].time] += sum(a[j].val - 1);
b[++k] = a[j++];
}
for (int p = l; p <= mid; p++) add(a[p].val, -1);
for (int p = l; p <= r; p++) a[p] = b[p];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
pos[x] = i;
}
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
a[i] = {i, pos[x], x};
}
CDQ1(1, n);
sort(a + 1, a + 1 + n);
CDQ2(1, n);
for (int i = 1; i <= n; i++) {
ans[i] += ans[i - 1];
cout << ans[i] << "\n";
}
return 0;
}
I-Namesolo拜师 解题报告
题目大意
1、添加一条连接 a, b的边;
2、询问a, b是否在同一个联通块内,回答1表示肯定,0表示否定。
。
解题思路
并查集模板题。
每次询问是否连通,只需要查是否在同一棵树内,一直向上询问直到得到树根是否相同即可。
加边前先判断是否连通,若不连通,将其中一个端点设为另一端点的双亲节点。
无优化:期望复杂度,最坏复杂度(链)
路径压缩:查询时将点连到祖先。
int find(int x) { return x == f[x] ? f[x] : f[x] = find(f[x]); }
期望复杂度,最坏复杂度
按秩合并:为减少树高,将秩小的节点的双亲节点设为秩大的节点。
inline void merge(int fx, int fy) {
if (fx == fy) return;
if (s[fx] > s[fy]) swap(fx, fy);
f[fx] = fy; s[fy] += s[fx];
}
期望复杂度,最坏复杂度
同时使用路径压缩和按秩合并,期望复杂度,最坏复杂度
此题需要卡常。效果比较显著的有快读、减法代替取模
#define getchar() (frS==frT&&(frT=(frS=frBB)+fread(frBB,1,1<<15,stdin),frS==frT)?EOF:*frS++)
char frBB[1<<15],*frS=frBB,*frT=frBB;
template<typename T>
inline void read(T &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c)){x=x*10+c-'0';c=getchar();}
}
if (ans >= mod) ans -= mod;
还尝试了其他卡常方法,但实际效果不太明显。
最快的一次为147ms
代码实现
//
// Created by vv123 on 2022/4/24.
//
#include <bits/stdc++.h>
#define swap(a,b) {int t = a; a = b; b = t;}
using namespace std;
const int N = 5e6 + 10;
const int mod = 1e9 + 7;
int f[N], s[N];
inline void init(int n) { for (int i = 1; i <= n; i++) f[i] = i, s[i] = 1; }
int find(int x) { return x == f[x] ? f[x] : f[x] = find(f[x]); }
inline void merge(int fx, int fy) {
if (fx == fy) return;
if (s[fx] > s[fy]) swap(fx, fy);
f[fx] = fy; s[fy] += s[fx];
}
#define getchar() (frS==frT&&(frT=(frS=frBB)+fread(frBB,1,1<<15,stdin),frS==frT)?EOF:*frS++)
char frBB[1<<15],*frS=frBB,*frT=frBB;
template<typename T>
inline void read(T &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c)){x=x*10+c-'0';c=getchar();}
}
int main() {
int n, m, ans = 0;
read(n); read(m); init(n);
while (m--) {
int op, x, y;
read(op); read(x); read(y);
int fx = find(x), fy = find(y);
if (op & 1) merge(fx, fy);
else {
ans = ((ans << 1) + (fx == fy));
if (ans >= mod) ans -= mod;
}
}
printf("%d\n", ans);
return 0;
}
J-魔法商店 解题报告
题目大意
给出n个商品的价值和n张纸币的面值,每张纸币可购买一个价值不大于其面值的商品。
最多修改k张纸币的面值,问最小花费,或判定无解。
。
解题思路
首先我们要让“未匹配数“尽量小,如果它大于k,则问题无解。一个简单的想法是,对每个商品,选择能够买下它的面值最小的纸币。
在问题有解的情况下,我们记录现有方案中每个纸币与其对应商品的差值delta,我们将剩余的修改次数k用于前k大的delta,即可得到最小花费。
一开始我将商品从小到大排序然后贪心选择、贪心修改,没有通过,看了题解后发现改成从大到小排序就过了。因为当多个商品抢夺一张纸币时,一定是把纸币留给价值较高的商品更优。因为这样在不减少匹配数的同时使得这张纸币的delta尽可能的小。
对以上贪心的感性理解:我们可以考虑一张二分图,左边是物品,右边是纸币。每个物品向价格不小于它的纸币连边。对于左边的点,如果一个商品选择的不是”能够买下它的面值最小的纸币“而是抢了别家的更大的纸币,则会出现增广路。显然一个商品价值越高连出的边数越少,对于右边的点,应当优先满足左边出度较小的结点(价值较高的商品)。
实现上一开始想用一个指针单调扫一遍,发现细节有点多。用multiset就很好写了。
代码实现
//
// Created by vv123 on 2022/4/26.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f;
int n, k, w[N], v[N], delta[N], used[N];
void print(int arr[]) {
for (int i = 1; i <= n; i++)
printf("%d ", arr[i]);
puts("");
}
multiset<int> s;
int main() {
long long ans = 0;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &w[i]), s.insert(w[i]);
for (int i = 1; i <= n; i++)
scanf("%d", &v[i]), ans += v[i];
//sort(w + 1, w + 1 + n);
sort(v + 1, v + 1 + n, greater<>());
memset(delta, 0x3f, sizeof delta);
for (int i = 1; i <= n; i++) {
auto it = s.lower_bound(v[i]);
if (it != s.end()) {
delta[i] = *it - v[i];
s.erase(it);
}
}
/*
for (int i = 1; i <= n; i++) {
while(w[p] >= v[i] && p >= 1) p--;
if (p < n && !used[p+1]) p++;
if (p == 1 && w[p] < v[i]) break;
if (w[p] < v[i]) continue;
used[p] = 1;
delta[i] = w[p] - v[i];printf("choose %d for %d\n", w[p], v[i]);
p--;
}
*/
sort(delta + 1, delta + 1 + n);
//print(delta);
int cnt = 0;
for (int i = 1; i <= n; i++) {
ans += delta[i];
if (delta[i] == INF) cnt++;
}
if (cnt > k) puts("NIE");
else {
for (int i = max(1, n - k + 1); i <= n; i++)
ans -= delta[i];
printf("%lld\n", ans);
}
return 0;
}
K-云海蝴蝶螺 解题报告
题目大意
矩阵单点修改、矩阵最值查询。
。
解题思路
二维线段树模板题。
先对x坐标建一棵线段树,每个节点是一棵y方向的线段树,维护最大和最小值的线段树存储在二维数组mx和mn中。
查询的过程相对简单,包括以下两种情况,时间复杂度
- x是叶节点,直接查询该节点对应的y树
- x是非叶节点,分别查询两棵子树
修改过程,时间复杂度
-
修改x树叶节点时直接修改该节点对应的y树
-
修改x树非叶节点分为两个步骤,第一步是递归修改x树上的子树,第二步是修改该节点对应的y树(对每一个y树上的节点,合并x树上左右子树的y树同一位置节点的答案)
代码实现
//
// Created by vv123 on 2022/5/5.
//
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 510;
const int INF = 0x3f3f3f3f;
int n, m, op;
//----------------2D_Segment_Tree------------------
#define mid (l + r >> 1)
#define ls u << 1, l, mid
#define rs u << 1 | 1, mid + 1, r
int xrt, xisleaf, mx[N << 2][N << 2], mn[N << 2][N << 2];
int x1, y1, x2, y2;
int resmx, resmn;
void AskInY(int u, int l, int r) {
if (y1 <= l && y2 >= r) {
resmx = max(resmx, mx[xrt][u]);
resmn = min(resmn, mn[xrt][u]);
return;
}
if (y1 <= mid) AskInY(ls);
if (y2 >= mid + 1) AskInY(rs);
}
void AskInX(int u, int l, int r) {
if (x1 <= l && x2 >= r) {
xrt = u;
AskInY(1, 1, n);
return;
}
if (x1 <= mid) AskInX(ls);
if (x2 >= mid + 1) AskInX(rs);
}
int x, y, k;
void PushUpY(int u) {
mx[xrt][u] = max(mx[xrt][u << 1], mx[xrt][u << 1 | 1]);
mn[xrt][u] = min(mn[xrt][u << 1], mn[xrt][u << 1 | 1]);
}
void PushUpX(int u) {
mx[xrt][u] = max(mx[xrt << 1][u], mx[xrt << 1 | 1][u]);
mn[xrt][u] = min(mn[xrt << 1][u], mn[xrt << 1 | 1][u]);
}
void UpdInY(int u, int l, int r) {
if (l == r) {
if (xisleaf) {//如果要更新的xrt是叶子,需单点修改xrt的y树
mx[xrt][u] = mn[xrt][u] = k;
return;
}
PushUpX(u);//如果要更新的xrt不是叶子,需合并xrt子树的答案
return;
}
if (y <= mid) UpdInY(ls);
else UpdInY(rs);
PushUpY(u);
}
void UpdInX(int u, int l, int r) {
if (l == r) { //如果要更新的xrt是叶子,直接单点修改xrt的y树
xrt = u; xisleaf = 1; UpdInY(1, 1, n);
return;
}
if (x <= mid) UpdInX(ls);
else UpdInX(rs);
xrt = u; xisleaf = 0; UpdInY(1, 1, n); //如果要更新的xrt不是叶子,就递归左右子树,合并答案
}
//-------------------------------------------------
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
x = i, y = j, cin >> k, UpdInX(1, 1, n);
cin >> m;
while (m--) {
cin >> op;
if (op == 1) {
cin >> x >> y >> k;
UpdInX(1, 1, n);
} else {
resmx = -INF; resmn = INF;
cin >> x1 >> y1 >> x2 >> y2;
AskInX(1, 1, n);
cout << resmx << " " << resmn << "\n";
}
}
return 0;
}
L-GAMERS的众数 解题报告
题目大意
给出一个长度为的序列和个操作。单点修改,求区间众数。
解题思路
此题可以使用回滚莫队来解决。
首先总结一下普通莫队(如果求的是"众数出现次数"可以用普通莫队):
莫队是一种暴力而优雅的算法。
具体来说,将序列分块,我们需要将询问离线并按l所在的块为第一关键字、r为第二关键字排序。
然后实现add和del两个函数,O(1)维护当前区间从边上添加、删去一个数后的答案,使得我们能够从[L,R]拓展到[L+1,R]、[L-1,R]、[L,R+1]、[L,R-1]。我们可以对同一个块内的询问通过一个空区间暴力转移而得到。(注意,为了简化对复杂度的分析,我用的方法和常见的普通莫队不太一样,其实类似于回滚莫队的处理)
设块的大小为,则块的数量为,考虑R的移动,每个块中的询问是单调的,最多进行n次,一共块,总的移动为;考虑L的移动,在同一块内每个询问的移动次数都不超过S,总移动次数不超过为。L、R的总移动次数为,由基本不等式可以知道,S取时,时间复杂度最优,为
说完普通莫队,说说为什么需要回滚莫队。
我们发现,如果只有add操作,区间众数是很容易维护的。
void add(int c) {
ccnt[cnt[c]]--;
cnt[c]++;
ccnt[cnt[c]]++;
if (cnt[c] > max_cnt || (cnt[c] == max_cnt && c < max_id)) {
max_id = c;
max_cnt = cnt[c];
}
}
而我们从区间中删去一个众数,且众数唯一(即上面代码中ccnt[c]=1)时,会导致众数的出现次数(max_cnt)减少1,但是很难确定众数(max_id)变成了哪一个。
能不能找到一种方法让避免del的缺陷?
这样,我们依然按照l所在的块为第一关键字、r为第二关键字将询问排序
设莫队区间为[pl,pr],询问区间为[l,r]
如果左端点访问到了新块b,则莫队区间初始化为[R[b]+1, R[b]] (R[b]表示b的右端点)
[l,r]在同一块内,则暴力求答案
[l,r]在不同块内,将[pl,pr]拓展为[l,r]:
- 先将pr拓展到r,记录此时的众数tmp
- 再将pl(从R[b]+1)拓展到l,求出该询问的众数。
- 将pl回滚到R[b]+1,将当前众数改回tmp
这样,我们避免了del对max_id的影响,所有新众数的产生都是从空区间经过add操作得到的。
设块的大小为S,则对[l,r]在同一块内的询问可以的时间计算。
对于其他询问:
左端点单次移动不超过,总共不超过。(其实算上回滚应该是2S...影响不大)
右端点单调递增,对左端点同块的询问移动次数不超过n,一共块,总共不超过
左右端点总移动次数为,由基本不等式可以知道,S取时,时间复杂度最优,为
代码实现
//
// Created by vv123 on 2022/4/25.
//
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int a[N], val[N], n, m, t, L[N], R[N], pos[N], ans[N];
struct query {
int l, r, block, id;
bool operator < (const query& x) const {
if (block != x.block) return block < x.block;//左端点所在区间
else return r < x.r;
}
} q[N];
inline void LiSanHua() {
for (int i = 1; i <= n; i++)
val[i] = a[i];
sort(val + 1, val + 1 + n);
m = unique(val + 1, val + 1 + n) - 1 - val;
for (int i = 1; i <= n; i++)
a[i] = lower_bound(val + 1, val + 1 + m, a[i]) - val;
}
inline void init_block() {
t = sqrt(n);
for (int i = 1; i <= t; i++) {
L[i] = (i - 1) * t + 1;
R[i] = i * t;
}
if (R[t] < n)
t++, L[t] = R[t - 1] + 1, R[t] = n;
for (int i = 1; i <= n; i++)
for (int j = L[i]; j <= R[i]; j++)
pos[j] = i;
}
int __cnt[N];
int bruteforce(int l, int r) {
int max_cnt = 0, max_id = N;
for (int i = l; i <= r; i++) ++__cnt[a[i]];
for (int i = l; i <= r; i++) {
if (__cnt[a[i]] > max_cnt || (__cnt[a[i]] == max_cnt && a[i] < max_id))
max_id = a[i], max_cnt = __cnt[a[i]];
}
for (int i = l; i <= r; i++) --__cnt[a[i]];
return max_id;
}
int ccnt[N], cnt[N], max_cnt = 0, max_id = N;//ccnt记录某个计数出现了几次
void add(int c) {
ccnt[cnt[c]]--;
cnt[c]++;
ccnt[cnt[c]]++;
if (cnt[c] > max_cnt || (cnt[c] == max_cnt && c < max_id)) {
max_id = c;
max_cnt = cnt[c];
}
}
void del(int c) {
ccnt[cnt[c]]--;
if (cnt[c] == max_cnt && ccnt[cnt[c]] == 0) max_cnt--;//max_cnt一定是连续的。max_id在拓展左端点前保存即可。
cnt[c]--;
ccnt[cnt[c]]++;
}
void print(int arr[]) {
for (int i = 1; i <= n; i++)
printf("%d ", arr[i]);
puts("");
}
inline void MoDui() {
/*
莫队区间[pl,pr],询问区间[l,r]
如果访问到了新块b,则莫队区间初始化为[R[b]+1, R[b]]
[l,r]在同一块内,则暴力扫描
[l,r]在不同块内,将[pl,pr]拓展为[l,r],然后将pl回滚到R[b]+1
*/
int pl = 1, pr = 0, last_block = 0;
for (int i = 1; i <= n; i++) {
int l = q[i].l, r = q[i].r, id = q[i].id;//printf("%d %d %d %d\n", l, r, q[i].block, q[i].id);
if (pos[l] == pos[r]) {
ans[id] = bruteforce(l, r);
continue;
}
if (pos[l] != last_block) {
while (pr > R[pos[l]]) del(a[pr--]);
while (pl < R[pos[l]] + 1) del(a[pl++]);
max_cnt = 0, max_id = N;
last_block = pos[l];
}
//右端点有序,左端点每次从回滚点向左拓展
//精髓在于,我们查询答案时,只会add而不会del
while (pr < r) add(a[++pr]);
int __pl = pl, tmp = max_id;//因为我们还会回到这个状态,因此只需暂存max_id即可
while (__pl > l) add(a[--__pl]);
ans[id] = max_id;
while (__pl < pl) del(a[__pl++]);//滚回R[b]+1
max_id = tmp;//滚回tmp
}
};
signed main() {
scanf("%lld", &n);
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
LiSanHua();//print(a); print(val);
init_block();
for (int i = 1; i <= n; i++) {
int l, r;
scanf("%lld%lld", &l, &r);
q[i] = {l, r, pos[l], i};
}
sort(q + 1, q + 1 + n);
MoDui();
for (int i = 1; i <= n; i++)
printf("%lld\n", val[ans[i]]);
return 0;
}
Z-卷卷人的计划表 解题报告
题目大意
一开始有N个数,进行M次操作:
1、在第 pos个数前插入一个数 k
2、查询第pos个数是什么
解题思路
我们可以用块状链表解决这个问题。
我们将最大数据量n分为sqrt(n)个节点,每个节点存最多sqrt(n)个数,用链表连接。
这样插入、查询操作都是O(sqrt(n))的。实现时,还需要在节点大小超过2*sqrt(n)的时候进行分裂操作,否则大量相同数据可能使复杂度退化到O(n)。
幸运的是,STL中的有块状链表rope可供使用,其常用操作如下
rope<int> a;
a.push_back(x) // 在末尾插入
a.pop_back(x) // 删除最后一个元素
a.size() // 返回长度
a.insert(int pos, x) // 在pos插入x
a.erase(int pos, int sum) // 在pos删除sum个元素
a.replace(int pos, x) // 将pos替换为x
a.substr(int pos, int sum) // 在pos处截取sum个元素
a.at(x) a[x] //访问第x个元素
代码实现
//
// Created by vv123 on 2022/4/28.
//
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
using namespace __gnu_cxx;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
rope<int> r;
int n, m;
cin >> n >> m;
while (n--) {
int x;
cin >> x;
r.push_back(x);
}
int op, pos, k;
while (m--) {
cin >> op;
if (op == 1) {
cin >> k >> pos;
r.insert(pos - 1, k);
} else {
cin >> pos;
cout << r.at(pos - 1) << "\n";
}
//for (auto x:r) printf("%d->", x);
//puts("");
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律