魔法导论

发布时间 2023-03-29 21:30:58作者: Houraisan_Kaguya

总是要留下一点什么的吧。

如果运气好没有 AFO 的话会持续更新(划掉)。

下面就记录一点没啥用的东西。

时间系

败者食尘

一个非常经典的 \(Trick\)
由于需要对时间进行回溯,所以考虑使用败者食尘。
当然,败者食尘是不能用来维护数列的,所以线段树必不可少。

具体的,首先用普通线段树维护序列。同时维护每个版本出现的时间点。
当我们需要版本回溯的时候,直接使用败者食尘精准回到相应的时间点即可。
然而,败者食尘的缺点在于,发生过的事情还会发生一遍。
也就是说,我们回溯的这段时间内对线段树的修改是无法抹除的。

所以,考虑每次修改新开一个节点。
当我们准备修改节点 \(a\) 的时候,保持节点 \(a\) 不变,新开一个节点 \(a'\) 并复制一份 \(a\) 的信息。
然后将 \(a\) 的父节点的子节点改成 \(a'\)
这样以来我们就保证了 \(a\) 节点的信息不会被直接修改。

容易发现时空复杂度都是 \(O(n \log n)\)

实现
#include <stdio.h>
#include <ctype.h>
#define sz 100001
const int md = 998244353;
struct site
{
	int ls, rs, sum, laz, cov, bs;
};
struct site tree[sz << 6 | 1];
int n, m, now, p;
int rot[sz];
int read();
int haf(int);
int build(int, int);
int fix(int, int, int, int, int, int, int);
int cvo(int, int, int, int, int, int, int);
int serch(int, int, int, int, int);
int main()
{
	int x, y, c;
	n = read(); m = read();
	rot[0] = build(1, n);
	while (m --> 0)
	{
		x = read();
		if (x < 3)
		{
			++now;
			if (x == 1)
			{
				x = read(); y = read(); c = read();
				rot[now] = fix(now, rot[now - 1], 1, n, x, y, c);
			}else
			{
				x = read(); y = read(); c = read();
				rot[now] = cvo(now, rot[now - 1], 1, n, x, y, c);
			}
		}else
		{
			if (x == 3)
			{
				x = read(); y = read();
				printf ("%d\n", serch(rot[now], 1, n, x, y));
			}else
			{
				now -= read();
				p = haf(now);
			}
		}
	}
	return 0;
}

int read()
{
	int x = 0;
	char c;
	while (!isdigit(c = getchar()));
	do {
		x = (x << 3) + (x << 1) + (c & 15);
	}while (isdigit(c = getchar()));
	return x;
}

int haf(int a)
{
	int lf = 1, rt = p + 1, mid;
	while (rt != lf + 1)
	{
		mid = lf + rt >> 1;
		if (tree[mid].bs <= a)
			lf = mid;
		else
			rt = mid;
	}
	return lf;
}

int build(int lf, int rt)
{
	++p;
	int a = p, mid;
	if (lf == rt)
		tree[a].sum = read();
	else
	{
		mid = lf + rt >> 1;
		tree[a].ls = build(lf, mid);
		tree[a].rs = build(mid + 1, rt);
		tree[a].sum = tree[tree[a].ls].sum + tree[tree[a].rs].sum;
		if (tree[a].sum >= md)
			tree[a].sum -= md;
	}
	return a;
}

#define push_down if(tree[x].cov){tree[x].ls=cvo(kd,tree[x].ls,lf,mid,lf,mid,tree[x].cov);tree[x].rs=cvo(kd,tree[x].rs,mid+1,rt,mid+1,rt,tree[x].cov);tree[x].cov=0;}if(tree[x].laz){tree[x].ls=fix(kd,tree[x].ls,lf,mid,lf,mid,tree[x].laz);tree[x].rs=fix(kd,tree[x].rs,mid+1,rt,mid+1,rt,tree[x].laz);tree[x].laz=0;}
int fix(int kd, int a, int lf, int rt, int wa, int wb, int b)
{
	int x, mid;
	if (tree[a].bs == kd)
		x = a;
	else
	{
		++p; x = p;
		tree[x] = tree[a];
		tree[x].bs = kd;
	}
	if (lf == wa && rt == wb)
	{
		if (tree[a].cov)
		{
			tree[x].cov += b;
			if (tree[x].cov >= md)
				tree[x].cov -= md;
		}else
		{
			tree[x].laz += b;
			if (tree[x].laz >= md)
				tree[x].laz -= md;
		}
		tree[x].sum = (tree[x].sum + (long long)b * (wb - wa + 1)) % md;
		return x;
	}
	mid = lf + rt >> 1;
	push_down;
	if (wb <= mid)
		tree[x].ls = fix(kd, tree[x].ls, lf, mid, wa, wb, b);
	else if (wa > mid)
		tree[x].rs = fix(kd, tree[x].rs, mid + 1, rt, wa, wb, b);
	else
	{
		tree[x].ls = fix(kd, tree[x].ls, lf, mid, wa, mid, b);
		tree[x].rs = fix(kd, tree[x].rs, mid + 1, rt, mid + 1, wb, b);
	}
	tree[x].sum = tree[tree[x].ls].sum + tree[tree[x].rs].sum;
	if (tree[x].sum >= md)
		tree[x].sum -= md;
	return x;
}

int cvo(int kd, int a, int lf, int rt, int wa, int wb, int b)
{
	int x, mid;
	if (tree[a].bs == kd)
		x = a;
	else
	{
		++p; x = p;
		tree[x] = tree[a];
		tree[x].bs = kd;
	}
	if (lf == wa && rt == wb)
	{
		tree[x].laz = 0;
		tree[x].cov = b;
		tree[x].sum = (long long)(wb - wa + 1) * b % md;
		return x;
	}
	mid = lf + rt >> 1;
	push_down;
	if (wb <= mid)
		tree[x].ls = cvo(kd, tree[x].ls, lf, mid, wa, wb, b);
	else if (wa > mid)
		tree[x].rs = cvo(kd, tree[x].rs, mid + 1, rt, wa, wb, b);
	else
	{
		tree[x].ls = cvo(kd, tree[x].ls, lf, mid, wa, mid, b);
		tree[x].rs = cvo(kd, tree[x].rs, mid + 1, rt, mid + 1, wb, b);
	}
	tree[x].sum = tree[tree[x].ls].sum + tree[tree[x].rs].sum;
	if (tree[x].sum >= md)
		tree[x].sum -= md;
	return x;
}

int serch(int a, int lf, int rt, int wa, int wb)
{
	if (tree[a].cov)
		return (long long)tree[a].cov * (wb - wa + 1) % md;
	if (lf == wa && rt == wb)
		return tree[a].sum;
	int mid = lf + rt >> 1;
	if (wb <= mid)
		return (serch(tree[a].ls, lf, mid, wa, wb) + (long long)tree[a].laz * (wb - wa + 1)) % md;
	else if (wa > mid)
		return (serch(tree[a].rs, mid + 1, rt, wa, wb) + (long long)tree[a].laz * (wb - wa + 1)) % md;
	return (serch(tree[a].ls, lf, mid, wa, mid) + serch(tree[a].rs, mid + 1, rt, mid + 1, wb) + (long long)tree[a].laz * (wb - wa + 1)) % md;
}

拓展应用:可持久化数组可持久化并查集Dynamic Rankings

绯红之王

如果要维护一些难以抹除贡献的信息,就要使用绯红之王直接对时间操作进行删除。

首先这道题的信息很怪,要用莫队。
但是维护最大值的时候是很难进行删除操作的。
与此同时,如果使用线段树维护或者败者食尘暴力回溯,会在复杂度上多一个 \(\log\)
这时候就要学一学绯红之王了。

对序列分块之后,我们依次枚举每一个块。
先让左右端点都位于块的右边,然后右端点单调右移更新信息。
当我们需要对左端点进行左移操作的时候,先单开一个线程发动绯红之王,跳出时间线。
同时,这个时间线上的程序正常运行,向左更新答案。
最后,记录一下答案,然后这段时间被删除,程序并没有意识到自己向左更新了。

总体复杂度是 \(O(n \sqrt n)\) 的。
远古代码,绯红之王还用的不是很熟练。

实现
#include <algorithm>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <math.h>
#define sz 200001
#define qsz 455
struct site
{
	int lf, rt, poi;
};
struct site dld[sz];
struct node
{
	int poi, yf1, yf2;
};
struct node sta[sz];
int n, m, big, now, lp, rp, tmpbig;
int num[sz], own[sz], st[qsz], ed[qsz], frt[sz], bak[sz], lst[sz];
int read();
bool cmp(const site&, const site&);
int haf(int);
int main()
{
	n = read(); big = sqrt(n);
	for (int i = 1; i <= n; ++i)
		num[i] = own[i] = read();
	std::sort(own + 1, own + 1 + n);
	own[0] = 2;
	for (int i = 2; i <= n; ++i)
		if (own[i] != own[i - 1])
		{
			own[own[0]] = own[i];
			++own[0];
		}
	for (int i = 1; i <= n; ++i)
		num[i] = haf(num[i]);
	m = read();
	for (int i = 1; i <= m; ++i)
	{
		dld[i].poi = i;
		dld[i].lf = read();
		dld[i].rt = read();
	}
	for (int i = 1, j = 1; i <= n; ++j)
	{
		st[j] = i;
		ed[j] = i + big;
		if (ed[j] > n)
			ed[j] = n;
		while (i <= ed[j])
		{
			own[i] = j;
			++i;
		}
	}
	std::sort(dld + 1, dld + 1 + m, cmp);
	for (int i = 1; i <= m; ++i)
	{
		if (i == 1 || own[dld[i].lf] != own[dld[i - 1].lf])
		{
			memset(frt + 1, 0, own[0] << 2);
			memset(bak + 1, 0, own[0] << 2);
			rp = ed[own[dld[i].lf]];
			lp = rp + 1; big = 0;
		}
		if (own[dld[i].lf] == own[dld[i].rt])
		{
			tmpbig = 0;
			for (int j = dld[i].lf; j <= dld[i].rt; ++j)
			{
				if (!lst[num[j]])
					lst[num[j]] = j;
				if (tmpbig < j - lst[num[j]])
					tmpbig = j - lst[num[j]];
			}
			for (int j = dld[i].lf; j <= dld[i].rt; ++j)
				lst[num[j]] = 0;
			dld[i].rt = tmpbig;
			continue;
		}
		while (rp < dld[i].rt)
		{
			++rp;
			if (!frt[num[rp]])
				frt[num[rp]] = rp;
			if (big < rp - frt[num[rp]])
				big = rp - frt[num[rp]];
			bak[num[rp]] = rp;
		}
		tmpbig = big;
		while (lp > dld[i].lf)
		{
			--lp; ++now;
			sta[now].poi = num[lp];
			sta[now].yf1 = frt[num[lp]];
			sta[now].yf2 = bak[num[lp]];
			if (!bak[num[lp]])
				bak[num[lp]] = lp;
			if (big < bak[num[lp]] - lp)
				big = bak[num[lp]] - lp;
			frt[num[lp]] = lp;
		}
		dld[i].rt = big; lp = ed[own[dld[i].lf]] + 1;
		big = tmpbig;
		while (now)
		{
			frt[sta[now].poi] = sta[now].yf1;
			bak[sta[now].poi] = sta[now].yf2;
			--now;
		}
	}
	for (int i = 1; i <= m; ++i)
		frt[dld[i].poi] = dld[i].rt;
	for (int i = 1; i <= m; ++i)
		printf ("%d\n", frt[i]);
	return 0;
}

int read()
{
	int x = 0;
	char c;
	while (!isdigit(c = getchar()));
	do {
		x = (x << 3) + (x << 1) + (c & 15);
	}while (isdigit(c = getchar()));
	return x;
}

bool cmp(const site &a, const site &b)
{
	if (own[a.lf] == own[b.lf])
		return a.rt < b.rt;
	return own[a.lf] < own[b.lf];
}

int haf(int a)
{
	int lf = 1, rt = own[0], mid;
	while (rt != lf + 1)
	{
		mid = lf + rt >> 1;
		if (own[mid] <= a)
			lf = mid;
		else
			rt = mid;
	}
	return lf;
}

拓展应用:歴史の研究

空间系

单向箔

常规的计算机只适合用来解决一维的问题。
如果问题的维度较高我们就需要考虑强行降维,物理解决问题。

首先拿到这个问题我们发现这个是在二维平面上的。
考虑使用单向箔。

首先在整个二维空间的边界处放置一个单向箔,从左向右对空间进行拆解。
同时使用树状数组维护拆解过程中每个位置接收的物质量。

这样就可以将一次区间求和拆解成两次接受的物质量求和并做差。

由于单向箔的扩展速度为光速,所以复杂度瓶颈主要在树状数组上。
需要注意的一点是,单向箔要少用,所以这道题尽量少交。

实现
#include <algorithm>
#include <stdio.h>
#define sz 300005
typedef long long ll;
struct site
{
	int x, y, val;
};
site poi[sz];
struct qus
{
	int kid, x, a, b;
	qus(){}
	qus(int a1, int a2, int a3, int a4)
	{
		kid = a1; x = a2;
		a = a3; b = a4;
	}
};
qus sqr[sz];
int n, m, mx;
int ls[sz << 1];
ll ans[sz], tree[sz];
int read();
void load();
int haf(int);
bool cmp1(const qus&, const qus&);
bool cmp2(const site&, const site&);
void sol();
void up(int, int);
ll down(int);
int main()
{
	int x, y, a, b;
	n = read(); m = read();
	for (int i = 1; i <= n; ++i)
	{
		poi[i].x = read();
		poi[i].y = read();
		poi[i].val = read();
	}
	for (int i = 1; i <= m; ++i)
	{
		x = read(); a = read();
		y = read(); b = read();
		sqr[(i << 1) - 1] = qus(i, x, a, b);
		sqr[i << 1] = qus(i, y, a, b);
	}
	m <<= 1; load();
	sol(); m >>= 1;
	for (int i = 1; i <= m; ++i)
		printf ("%lld\n", ans[i]);
	return 0;
}

int read()
{
	int x = 0;
	char c = getchar(), f = 0;
	while (c < '0')
	{
		f = (c == '-');
		c = getchar();
	}
	do {
		x = x * 10 + (c & 15);
		c = getchar();
	}while (c >= '0');
	return f ? -x : x;
}

void load()
{
	for (int i = 1; i <= n; ++i)
		ls[++ls[0]] = poi[i].x;
	for (int i = 1; i <= m; ++i)
		ls[++ls[0]] = sqr[i].x;
	std::sort(ls + 1, ls + 1 + ls[0]);
	mx = 1;
	for (int i = 2; i <= ls[0]; ++i)
		if (ls[i] != ls[mx])
			ls[++mx] = ls[i];
	ls[0] = mx;
	for (int i = 1; i <= n; ++i)
		poi[i].x = haf(poi[i].x);
	for (int i = 1; i <= m; ++i)
		sqr[i].x = haf(sqr[i].x) - (i & 1);
	ls[0] = 0;
	for (int i = 1; i <= n; ++i)
		ls[++ls[0]] = poi[i].y;
	for (int i = 1; i <= m; i += 2)
	{
		ls[++ls[0]] = sqr[i].a;
		ls[++ls[0]] = sqr[i].b;
	}
	std::sort(ls + 1, ls + 1 + ls[0]);
	mx = 1;
	for (int i = 2; i <= ls[0]; ++i)
		if (ls[i] != ls[mx])
			ls[++mx] = ls[i];
	ls[0] = mx;
	for (int i = 1; i <= n; ++i)
		poi[i].y = haf(poi[i].y);
	for (int i = 1; i <= m; ++i)
	{
		sqr[i].a = haf(sqr[i].a);
		sqr[i].b = haf(sqr[i].b);
	}
}

int haf(int a)
{
	int lf = 1, rt = ls[0], mid;
	while (rt > lf)
	{
		mid = lf + rt >> 1;
		if (ls[mid] >= a)
			rt = mid;
		else
			lf = mid + 1;
	}
	return rt;
}

bool cmp1(const qus &a, const qus &b)
{
	return a.x < b.x;
}

bool cmp2(const site &a, const site &b)
{
	return a.x < b.x;
}

void sol()
{
	std::sort(sqr + 1, sqr + 1 + m, cmp1);
	std::sort(poi + 1, poi + 1 + n, cmp2);
	for (int i = 1, j = 1; j <= m; )
		if (i <= n && poi[i].x <= sqr[j].x)
		{
			up(poi[i].y, poi[i].val);
			++i;
		}else
		{
			ans[sqr[j].kid] = down(sqr[j].b) - down(sqr[j].a - 1) - ans[sqr[j].kid];
			++j;
		}
}

void up(int a, int b)
{
	while (a <= mx)
	{
		tree[a] += b;
		a += a & -a;
	}
}

ll down(int a)
{
	ll ret = 0LL;
	while (a)
	{
		ret += tree[a];
		a ^= a & -a;
	}
	return ret;
}

拓展应用:Mokia窗口的星星
窗口的星星那道题还需要透视法,请读者自学。

空间扭转

在著名的魔导书《线性代数》中有介绍过线性空间。
这道题经过问题抽象之后就是,给定一个线性空间,求最大子空间。

显然,我们可以通过对空间进行简单的扭转来解决问题。
但是这个操作有一点复杂,所以我们需要线性基来维护。

总的来说,一个线性空间是由一组线性基来维持的。
对线性基进行的修改/破坏会直接映射到空间中去。

具体的做法直接参考 魔法网站

老样子,贴一个代码。

实现
#include <stdio.h>
#include <ctype.h>
int n;
long long bs[65];
long long read();
int main()
{
	long long x;
	n = read();
	for (int i = 1; i <= n; ++i)
	{
		x = read();
		for (int j = 50; j >= 0; --j)
			if (x & 1ll << j)
			{
				if (!bs[j])
				{
					bs[j] = x;
					break;
				}
				x ^= bs[j];
			}
	}
	x = 0;
	for (int i = 50; i >= 0; --i)
		if (!(x & 1ll << i))
			x ^= bs[i];
	printf ("%lld\n", x);
	return 0;
}

long long read()
{
	long long x = 0;
	char c;
	while (!isdigit(c = getchar()));
	do {
		x = (x << 3) + (x << 1) + (c & 15);
	}while (isdigit(c = getchar()));
	return x;
}