2023.7.4 数据结构专题一

T1 Messenger Simulator

luogu link
首先对于第一问非常好做 因为如果收到消息 就是 \(1\) 否则它只会一直往回退 就是它本身
考虑第二问 我们发现 每次将 \(x\) 提前 那么它前面所有数的位置都会回退一格 所以我们考虑开个线段树或者 \(BIT\) 维护都行 可以将初始位置都加 \(3 * 10^5\) 暴力模拟 然后每次提前的时候要把它的答案更新一下

点击查看代码
#include <bits/stdc++.h>
#define ls (k << 1)
#define rs (k << 1 | 1)
#define mid (l + r >> 1)
using namespace std;

const int N = 3e5 + 0721;
int lazy[N << 3];
int ans[N], vis[N], newest[N];
int n, m, h, t;

void init() {
	t = n + 3e5;
	h = 1 + 3e5;
	for (int i = 1; i <= n; ++i) newest[i] = i + 3e5, ans[i] = i;
}

void pushdown(int k) {
	lazy[ls] += lazy[k];
	lazy[rs] += lazy[k];
	lazy[k] = 0;
}

void modify(int k, int l, int r, int u, int v) {
//	cout << l << " " << r << "\n";
	if (l >= u && r <= v) {
		++lazy[k];
		return;
	}
	if (lazy[k] != 0) pushdown(k);
	if (u <= mid) modify(ls, l, mid, u, v);
	if (v > mid) modify(rs, mid + 1, r, u, v);
}

int get_ans(int k, int l, int r, int loc) {
	if (l == r) {
		return lazy[k];
	}
	if (lazy[k] != 0) pushdown(k);
	if (loc <= mid) return get_ans(ls, l, mid, loc);
	else return get_ans(rs, mid + 1, r, loc);
}

int main() {
	scanf("%d%d", &n, &m);
	init();
	for (int i = 1; i <= m; ++i) {
		int x;
		scanf("%d", &x);
//		cout << h << " " << newest[x] - 1<< "\n";
		if (h != newest[x]) modify(1, 1, t, h, newest[x] - 1);
		if (!vis[x]) ans[x] = max(ans[x], get_ans(1, 1, t, newest[x]) + ans[x]);
		else ans[x] = max(ans[x], get_ans(1, 1, t, newest[x]) + 1);
//		cout << newest[1] << ' ' << get_ans(1, 1, t, newest[1]) << '\n';
		--h;
		newest[x] = h;
		vis[x] = 1;
	}
	for (int i = 1; i <= n; ++i) {
		if (!vis[i]) ans[i] = max(ans[i], get_ans(1, 1, t, newest[i]) + ans[i]);
		else ans[i] = max(ans[i], get_ans(1, 1, t, newest[i]) + 1);
		if (vis[i]) printf("1 %d\n",ans[i]);
		else printf("%d %d\n", i, ans[i]);
	}

	return 0;
}

T2 New Year and Conference

luogu link
首先直接枚举这个子集合显然是不可行的 我们考虑缩小规模
发现如果子集合中对于每个冲突 必然是发生在两个集合之间的 所以我们枚举两个集合 让它们在一边冲突一边不冲突即可 显然不会漏情况
但是不能暴力枚举 这是个区间问题 所以我们先考虑按会场一的左端点排序让其一维有序 再解决另一维
我们考虑枚举在会场一不重合的区间 看它们在会场二是否重合
我们用一根扫描线把会场一的区间从左往右扫 假如说我现在的扫描线位置与区间 \(x\) 的左端点重合
那么所有右端点被扫描线扫过的区间 都是与 \(x\) 在左侧不相交的区间
而与 \(x\) 在右侧不相交的区间 扫到那就会把 \(x\) 统计进去了所以无所谓
那么我们考虑 扫描线每扫到一个右端点 就把它在会场二所对应的区间都 \(+1\)
然后判断 \(x\) 在会场二对应的区间最大值/区间和为不为 \(0\) 即可判断它们在会场二是否冲突

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define int long long
#define ls (k << 1)
#define rs (k << 1 | 1)
#define mid (l + r >> 1)
#define debug cout << "check\n"
using namespace std;

const int N = 2e5 + 0721;
int a[N << 2], cnt;
int n, m;

struct node {
	int sa, sb, ea, eb;
	friend bool operator<(node x, node y) {
		return x.sa < y.sa;
	}
} lec[N];

struct tree {
	ll tr[N << 4], lazy[N << 4];
	
	void plus(int k, int l, int r, int v) {
		lazy[k] += v;
	}
	
	void pushdown(int k) {
		lazy[ls] += lazy[k];
		lazy[rs] += lazy[k];
		lazy[k] = 0;
	}
	
	void pushup(int k) {
		tr[k] = max(tr[ls] + lazy[ls], tr[rs] + lazy[rs]);
	}
	
	void modify(int k, int l, int r, int u, int v) {
		if (u <= l && v >= r) {
			plus(k, l, r, 1);
			return;
		}
		pushdown(k);
		if (u <= mid) modify(ls, l, mid, u, v);
		if (v > mid) modify(rs, mid + 1, r, u, v);
		pushup(k);
	}
	
	ll query(int k, int l, int r, int u, int v) {
		if (u <= l && v >= r) {
			return tr[k] + lazy[k];
		}
		pushdown(k);
		ll ans = 0;
		if (u <= mid) ans = max(ans, query(ls, l, mid, u, v));
		if (v > mid) ans = max(ans, query(rs, mid + 1, r, u, v));
		pushup(k);
		return ans;
	}
} seg;

void lsh() {
	
	sort(a + 1, a + 1 + cnt);
	m = unique(a + 1, a + 1 + cnt) - a;
	for (int i = 1; i <= n; ++i) {
		lec[i].sa = lower_bound(a + 1, a + 1 + m, lec[i].sa) - a - 1;
		lec[i].sb = lower_bound(a + 1, a + 1 + m, lec[i].sb) - a - 1;
		lec[i].ea = lower_bound(a + 1, a + 1 + m, lec[i].ea) - a - 1;
		lec[i].eb = lower_bound(a + 1, a + 1 + m, lec[i].eb) - a - 1;
	}
}

vector<int> vr[N << 2];
vector<int> vl[N << 2];

bool solve() {
	for (int i = 0; i <= m + 2; ++i) {
		vr[i].clear();
		vl[i].clear();
	} 
	
	memset(seg.tr, 0, sizeof seg.tr);
	memset(seg.lazy, 0, sizeof seg.lazy);
	
	sort(lec + 1, lec + 1 + n);
	
	for (int i = 1; i <= n; ++i) {
		int l = lec[i].sa, r = lec[i].ea;
		vl[l].push_back(i);
		vr[r].push_back(i);
	}
	for (int i = 0; i <= m + 2; ++i) {
		if (vl[i].size()) {
			for (int j = 0; j < vl[i].size(); ++j) {
				int id = vl[i][j];
				if (seg.query(1, 0, m + 2, lec[id].sb, lec[id].eb) != 0) return 1;
			}
		}
		if (vr[i].size()) {
			for (int j = 0; j < vr[i].size(); ++j) {
				int id = vr[i][j];
				seg.modify(1, 0, m + 2, lec[id].sb, lec[id].eb);
			}
		}
	}
	return 0;
}

signed main() {
	
	scanf("%lld", &n); 
 	for (int i = 1; i <= n; ++i) {
 		scanf("%lld%lld%lld%lld", &lec[i].sa, &lec[i].ea, &lec[i].sb, &lec[i].eb);
 		a[++cnt] = lec[i].sa;
 		a[++cnt] = lec[i].ea;
 		a[++cnt] = lec[i].sb;
 		a[++cnt] = lec[i].eb; 		
	}
	lsh();
	
	if (solve()) {
		printf("NO");
		return 0;
	}
	
	for (int i = 1; i <= n; ++i) {
		swap(lec[i].sa, lec[i].sb);
		swap(lec[i].ea, lec[i].eb);
	}
		
	if (solve()) {
		printf("NO");
	} else
		printf("YES");
	
	return 0;
}

T3 Lexicographically Small Enough

luogu link

因为 \(S\)\(T\) 长度相同 我们考虑 \(S <T\)
因为是字典序 所以按位比较即可
那么一定就是类似于前面一段都相同 然后这一位的 \(s_i < t_i\)
我们考虑怎么换
首先如果这一位已经有 \(s_i < t_i\) 了 那就不用换了 直接统计答案
否则我们考虑把 \(s\) 串中离 \(i\) 最近的还小于 \(t_i\) 的字母换过去
假设它的位置是 \(loc\) 那么代价就是 \(loc - i\)
然后统计答案
当然也可以让这一位 \(s_i = t_i\) 然后推到下一位
也是如果本来就有就不用换了
否则查询 \(t_i\) 这个字母在 \(s\) 串中最早出现的位置 然后给它换过来
注意这样换不仅要统计代价 因为要继续往下推还要考虑对别的字母的影响
显然会让 \(\left[i + 1, loc - 1\right]\) 这些位置的字母统一 \(+1\)
那么它就等价于让 \(\left[loc + 1, len - 1\right]\) 区间的统一 \(-1\)
所以开个线段树或者 \(BIT\) 维护偏移值 查询的时候加上这个偏移值

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 1e5 + 0721;
const ll inf = 0x7ffffffffffffff;
int n, T;
ll ans, equ;
char s[N], t[N];

struct node { 
    int tr[N];

    inline int lowbit(int x) {
        return x & (-x);
    }

    void update(int x, int val) {
        while (x <= n) {
            tr[x] += val;
            x += lowbit(x);
        }
    }

    ll query(int x) {
        ll ret = 0;
        while (x) {
            ret += tr[x];
            x -= lowbit(x);
        }
        return ret;
    }
} bit;

int main() {

    cin >> T;

    while (T--) {
        //  cout << "T=" << T << " ";
        queue<int> q[26];
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) cin >> s[i];
        for (int i = 1; i <= n; ++i) cin >> t[i];
        // cout << s + 1<< " " << t  + 1<< "\n";
        ans = inf, equ = 0;
        for (int i = 1; i <= n; ++i) bit.tr[i] = 0;
        for (int i = 1; i <= n; ++i) {
//            cout << s[i] << " " << s[i] - 'a' << "\n";
            q[s[i] - 'a'].push(i);
        }
        for (int i = 1; i <= n; ++i) bit.update(i, 1);
//         cout << "check";

        for (int i = 1; i <= n; ++i) {
//            cout << "i = " << i << " " << ans << " " << T << "\n";
            int loc = n + 1;
            int ch = t[i] - 'a';
//            cout << t[i] << " " << t[i] - 'a' << "\n";
            for (int j = 0; j < ch; ++j) {
//            	cout << j << " " << "check\n";
                if (!q[j].empty()) loc = min(loc, q[j].front());
            }
            if (loc != n + 1) ans = min(ans, equ + bit.query(loc - 1));

            if (q[ch].empty()) break;
            loc = q[ch].front();
            q[ch].pop();
            equ += bit.query(loc - 1);
            bit.update(loc, -1);
//            cout << "i = " << i << " " << ans << " " << T << "\n";
        }

        printf("%lld\n", (ans == inf ? -1 : ans));
    }

    return 0;
}

T4 「Wdsr-3」令人感伤的红雨

luogu link

非常头疼的阅读理解题
因为重点在于记录我不会的东西 然后由于这个 \(\Omega\) 函数我自己做的时候推出来了 所以略过不讲
这里给出结论:\(\Omega(l, r) = max(0, l - A(1, r))\)
并且 \(A(1, r)\) 表示在 \(\left[1, r\right]\) 区间内最大值的最大编号
重点说一下后续做法
因为每次加操作加的是一个非负数 那么它只会让前面那段区间最大值变得越来越大
所以它就有可能覆盖掉后面一些区间的区间最大值编号
我们注意到整个数列的 \(A(1, r)\) 显然是单点不减的
所以我们直接把每个能成为任意 \(A(1, r)\) 的编号都拎出来 并且把它们当做它们一段后继区间的父亲
这样每次区间加就查询当前区间内最靠后的一个代表点 然后比较它和下一个代表点的差值
如果加的这个值大于这个差值 就把下一个代表点吞掉 父亲连上 链表修改 继续往下查
然后查询的时候路径压缩即可

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 6e6 + 0721;
int fa[N], dis[N], nxt[N];
int a[N];
int n, q, maxn, maxid;

int find(int x) {
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

int main() {
	scanf("%d%d", &n, &q); 
	for (int i = 1; i <= n; ++i) {
		scanf("%d", &a[i]);
		if (a[i] >= maxn) {
			nxt[i] = n + 1;
			dis[maxid] = a[i] - maxn;
			nxt[maxid] = i;
			maxn = a[i];
			maxid = i;
		}
		fa[i] = maxid;
	}
	
	while (q--) {
		int opt, l, r;
		scanf("%d%d%d", &opt, &l, &r);
		if (opt == 1) {
			int fl = find(l);
			int tmp;
			dis[fl] -= r;
			while (dis[fl] < 0 && nxt[fl] <= n) {
				tmp = nxt[fl];
				nxt[fl] = nxt[tmp];
				dis[fl] += dis[tmp];
				fa[tmp] = fl;
				dis[tmp] = -1;
				nxt[tmp] = n + 1;
			}
		} 
		else {
			int fr = find(r);
			if (fr >= l) printf("0\n");
			else printf("%d\n",l - fr);
		}
	}

	return 0;
}

T5 [USACO20OPEN] Favorite Colors G

luogu link

首先我们不难发现这样一个事情:由同一个点出发到达的点必然都是同一个颜色
那我们如果考虑将它们合并 那么由这些同色的点出发到达的所有点必然也都是同一个颜色
所以我们考虑这样一直合并下去 直到所有点的出度 \(<2\)
合并的时候考虑启发式合并 就可以做到 \(O(nlogn)\)
或者可以链表缩小复杂度(?

  • 启发式合并 合完之后记得删掉原来的边 记得合的是 \(fa_x\)\(fa_y\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;

namespace steven24 {
	
const int N = 2e5 + 0721;
vector<int> e[N];
vector<int> son[N];  
int fa[N];
int col[N], cnt;
int n, m;
queue<int> q;

inline int read() {
	int xr = 0, F = 1;
	char cr;
	while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
	while (cr >= '0' && cr <= '9')
		xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
	return xr * F;
}

void write(int x) {
	if (x < 0) putchar('-'), x = -x;
	if (x >= 10) write(x / 10);
	putchar(x % 10 + '0');
}

void merge(int x, int y) {
	x = fa[x], y = fa[y];
	if (son[x].size() < son[y].size()) swap(x, y);
	for (int v : son[y]) {
		fa[v] = x;
		son[x].emplace_back(v);
	}
	for (int v : e[y]) {
		e[x].emplace_back(v);
	}
	if (e[x].size() > 1) q.push(x);
}

void main() {
	n = read(), m = read();
	for (int i = 1; i <= m; ++i) {
		int x = read(), y = read();
		e[x].emplace_back(y);
	}
	
	for (int i = 1; i <= n; ++i) {
		son[i].emplace_back(i);
		fa[i] = i;
		if (e[i].size() > 1) q.push(i);
	}
	
	while (!q.empty()) {
		int now = q.front();
		q.pop();
		int s = e[now][0];
		for (int i = 1; i < (int)e[now].size(); ++i) {
			int y = e[now][i];
			if (fa[y] == fa[s]) continue;
			merge(s, y);
		}
		while (e[now].size() > 1) e[now].pop_back();
	}
	
	for (int i = 1; i <= n; ++i) {
		if (!col[fa[i]]) col[fa[i]] = ++cnt;
		write(col[fa[i]]), putchar('\n');
	}
}	
	
	
}

int main() {
//	freopen("P6279_2.in", "r", stdin);
//	freopen("P6279.out", "w", stdout);
	steven24::main();
	return 0;
}
/*
9 12
1 2
4 2
5 8
4 6
6 9
2 9
8 7
8 3
7 1
9 4
3 5
3 4
*/

T6 Guessing Permutation for as Long as Possible

luogu link

要求在所有的询问之前都无法得到答案
考虑一个三元组 \((x, y, z)\) 显然对于这个三元组的第三个询问而言 前两个询问无法推得其询问结果
我们分情况讨论:

  • \((x, z)\) 为最后一次询问 那么如果 \((x, y)\)\((y, z)\) 的询问得到的大小关系相同 那么直接就能推出 所以此时前两个询问得到的大小关系一定不同
  • \((x, y)\) 为最后一次询问 那么如果 \((x, z)\)\((y, z)\) 的询问得到的大小关系不同 那么直接就能推出 所以此时前两个询问得到的大小关系一定相同
  • \((y, z)\) 为最后一次询问 那么如果 \((x, y)\)\((x, z)\) 的询问得到的大小关系不同 那么直接就能推出 所以此时前两个询问得到的大小关系一定相同

那么此时我们就得到了类似于 “某两个元素之间的大小关系必须相同 某两个元素之间的大小关系不同” 的信息
我们直接使用扩展域并查集 维护对于每个询问来说 该次询问对应的两个元素之间的大小关系
那么就会得到很多很多连通块 相同块里的询问对应的两个元素之间的大小关系必须相同
连通块与连通块之间的取值是没有影响的 所以最终答案就是 \(2^{连通块的数量}\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;

namespace steven24 {
	
const int N = 521;
const int M = 5e5 + 0721;
const int mod = 1e9 + 7;
int q[N][N];
int fa[M];
bool vis[M];
bool cal[M];
int n, m;

inline int read() {
	int xr = 0, F = 1;
	char cr;
	while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
	while (cr >= '0' && cr <= '9')
		xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
	return xr * F;
}

void init() {
	for (int i = 1; i <= (m << 1); ++i) fa[i] = i;
}

int find(int x) {
	return x == fa[x] ? x : fa[x] = find(fa[x]);
}	

int ksm(int x, int y) {
	int ret = 1;
	while (y) {
		if (y & 1) ret = 1ll * ret * x % mod;
		x = 1ll * x * x % mod;
		y >>= 1;
	}
	return ret;
}

void main() {
	n = read();
	m = (n * (n - 1) >> 1);
	for (int i = 1; i <= m; ++i) {
		int x = read(), y = read();
		q[x][y] = q[y][x] = i;
	}
	init();
	
	for (int i = 1; i <= n; ++i) {
		for (int j = i + 1; j <= n; ++j) {
			for (int k = j + 1; k <= n; ++k) {
					int x = q[i][j], y = q[j][k], z = q[i][k];
					if (z > x && z > y) { //(i, k)最后问 说明(i, j)和(j, k)的两个询问大小关系不同 
						int fx = find(x), fy = find(y), fmx = find(x + m), fmy = find(y + m);
						if (fx == fy) {
							printf("0\n");
							return;
						}
						if (fy == fmx) continue;
						fa[fx] = fmy;
						fa[fmx] = fy;
					} else if (x > y && x > z) { //(i, j)最后问 说明(i, k)和(j, k)的两个询问大小关系相同 
						int fy = find(y), fz = find(z), fmy = find(y + m), fmz = find(z + m);
						if (fy == fmz) {
							printf("0\n");
							return;
						}
						if (fy == fz) continue;
						fa[fy] = fz;
						fa[fmy] = fmz;
					} else { //(j, k)最后问 说明(i, j)和(i, k)的两个询问大小关系相同 
						int fx = find(x), fz = find(z), fmx = find(x + m), fmz = find(z + m);
						if (fx == fmz) {
							printf("0\n");
							return;
						}
						if (fx == fz) continue;
						fa[fx] = fz;
						fa[fmx] = fmz;
					}
			}
		}
	}
	
	int cnt = 0;
	for (int i = 1; i <= m; ++i) if (fa[i] == i) ++cnt;
	printf("%d\n", ksm(2, cnt));
}
	
}

int main() {
	steven24::main();
	return 0;
}
/*
4
2 1
4 1
3 1
2 4
4 3
2 3
*/

T7 生日礼物

首先我们考虑简化问题 首先把连续的正数和连续的负数全连在一起 这肯定是没有问题的
那么现在就得到了一个正负相间的区间
如果正区间的数量 \(\le m\) 那全选上就行
我们现在考虑 \(>m\) 的情况
考虑选或者不选每个数对于答案的贡献
我们发现如果选上一个负数 我们就可以减少一个正区间 带来的代价就是这个负数的大小
如果不选一个正数 带来的代价就是它的相反数 也可以减少一个正区间
所以我们按绝对值大小排序 从小到大选择合并 直到正区间的数量为 \(m\)


T8 [NOIP2016 提高组] 蚯蚓

luogu link

首先要考虑生长标记的处理 我们肯定不能给其余每个都暴力加上 \(q\)
但是我们发现 不加 \(q\) 的显然只有一条(或者说是切开的两条)
本着“你亏了就是我赚了”的原则 我们可以单独给这一条减去 \(q\)
然后给整体加上 \(q\)
这样查询最大值的时候直接加上这个懒标记即可
查询的时候因为每次拿出来一个 送回去两个 想到优先队列
但是这题的数据范围需要一个线性做法
首先我们考虑这样一件事 假如我切开了最长那条蚯蚓 剩下了 \(x_1\)\(y_1\)
然后我下一次再找最长的 因为凡是被切开的 有且仅一次少了 \(q\) 的长度
所以无论怎么切 都会有这次切的蚯蚓的原长小于等于上一次的
那么切下来的两半显然满足单调性
所以我们开三个队列维护 每次取出队首的三个元素比较即可

点击查看代码
/*
首先输入所有蚯蚓 然后排序
然后每次切 取三个队头比较 并且记录最大值出现在哪个队列里
将该蛇取出 加上懒标记并剪断
懒标记 += q
剪断的两条蛇分别减去懒标记
*/
#include <bits/stdc++.h>
#define ll long long
using namespace std;

inline int read() {
	int xr = 0, F = 1;
	char cr;
	while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
	while (cr >= '0' && cr <= '9')
		xr = (xr << 1) + (xr << 3) + (cr ^ 48), cr = getchar();
	return xr * F;
}

void write(ll x) {
	int wtop = 0;
	char ws[51];
	if (x < 0) x = -x, putchar('-');
	do {
		ws[++wtop] = x % 10 + '0';
		x /= 10;
	} while (x);
	for (int i = wtop; i; --i) putchar(ws[i]);
}

namespace steven24 {
	
const int N = 1e5 + 0721;
const ll inf = 0x7fffffffffffffff;
int a[N];
int n, m, q, u, v, t;
ll lazy;
double p;	

deque<ll> dq[4];
vector<ll> ans;

inline bool cmp(ll x, ll y) {
	return x > y;
}
	
void main() {
	n = read(), m = read(), q = read(), u = read(), v = read(), t = read();
	p = 1.0 * u / v;
	for (int i = 1; i <= n; ++i) a[i] = read();
	sort(a + 1, a + 1 + n, cmp);
	for (int i = 1; i <= n; ++i) dq[1].push_back(a[i]);
	
	for (int i = 1; i <= m; ++i) {
		ll maxn = -inf;
		int maxid;
		for (int j = 1; j <= 3; ++j) {
			if (!dq[j].empty() && dq[j].front() > maxn) {
				maxn = dq[j].front();
				maxid = j;
			}
		} 
		dq[maxid].pop_front();
		
		maxn += lazy;
		int x1 = floor(maxn * p);
		int x2 = maxn - x1;
		lazy += q;
		x1 -= lazy, x2 -= lazy;
		dq[2].push_back(x1);
		dq[3].push_back(x2);
		if (i % t == 0) write(maxn), putchar(' ');
	}
	putchar(' '), putchar('\n');
	
	for (int i = 1; i <= 3; ++i) {
		for (int j = 0; j < dq[i].size(); ++j) ans.push_back(dq[i][j] + lazy);
	}
	sort(ans.begin(), ans.end(), cmp);
	for (int j = t; j <= ans.size(); j += t) write(ans[j - 1]), putchar(' ');
	putchar(' '), putchar('\n');
}
	
}

int main() {
	steven24::main();
	return 0;
}

T9 [CSP-S2020] 贪吃蛇

luogu link

考虑递归思考的这么一件事
首先如果当前最长的蛇吃了最短的蛇 没有变成最短的蛇
那么如果第二长的蛇吃 必是吃了第二短的蛇 那么一定会剩下更短
那么第二长的蛇一定会想尽办法保证不死 那第一条蛇就可以放心地吃
如果吃完变成了最短的蛇 我们考虑下一条蛇的选择
如果它吃了 它也变成了最短的蛇 那么就需要考虑第三条的选择
假如第三条发现 它吃了不会变成最短的 那么它就会放心吃
那么第二条一定不会去吃
那么第一条就可以放心地吃
发现这就是一个奇偶性的问题了
所以首先 我们一直让它吃直到它吃完会变成最短
然后我们往下一点点考虑 直到发现有一条蛇吃完不会变得最短
然后就可以根据奇偶性来判断这条蛇的选择了
因为我们要每次拿出最长的和最短的 很容易想到用二叉堆
但是还是要线性做法
然后我们还是发现吃完剩一段的蛇还是具有单调性的
所以还是可以开两个队列来维护


T10 Omkar and Time Travel

luogu link

一切都是命运石之门的选择!

选择完了开始头疼 非常的头疼
区间问题考虑起手排序 这题显然右端点先到的先被传送 所以按右端点排好一点
\(f_i\) 表示第 \(i\) 个传送门从右端点传送回去再走回来的传送次数
因为传送回去了所有左端点大于 \(a_i\) 的传送门就需要再走一遍
所以 \(f_i\) 实际上就是所有右端点小于 \(b_i\) 并且左端点大于 \(a_i\) 的区间的 \(f\) 之和
不难发现这是一个经典的二维偏序
然后我们考虑怎么统计答案
不难发现 \(f\) 数组还可以表示第一次走到传送门右端点的传送次数
发现对于集合 \(S\) 中的每个传送门 我们最好先走到最靠右的那个 然后再也不往那边走
那么第一次的显然要依次经过右端点小于它的右端点的所有传送门并且被强制传送回去
那就是右端点小于它的 \(f\) 之和
然后我们回到了 \(a_i\) 现在要走到剩下的没被标记的最右的传送门
那么传送次数就是右端点小于它并且左端点大于 \(a_i\)\(f\) 之和
还是个二维偏序

posted @ 2023-07-04 19:44  Steven24  阅读(28)  评论(0编辑  收藏  举报