2024西安铁一中集训DAY22 ---- 模拟赛(简单dp + 线段树优化dp + 线段树优化dp + 搜索(点双优化))

时间安排与成绩

  • 7:40 开题
  • 7:40 - 8:00 看完T1,感觉很一眼。写了个 n 3 n^3 n3 的三维dp,没过样例
  • 8:00 - 8:10 感觉要多加两维状态,改了改,把样例过了。卡了卡空间。扔了看下一题
  • 8:10 - 8:13 把T2看了,肚子疼,去厕所里想
  • 8:13 - 8:18 在厕所里把题意想清楚了,感觉是一个类似区间覆盖的问题。应该可以dp,有点没想清楚
  • 8:18 - 8:20 先开T3,发现T3部分分很一眼,然后把特殊性质拓展一下就是正解了
  • 8:20 - 9:00 把T3暴力写完了,检查没有正确性的问题,然后把正解写完了。正解过大样例了。
  • 9:00 - 9:20 把对拍写了,开拍没多久就拍出来一组数据,赶紧调了调,发现vec里忘插入元素了,改完就过了。大样例太水了。
  • 9:20 - 9:40 想到了T2的一种简单写法,单 l o g log log。但是时限只有 200 m s 200ms 200ms???,不管先写了。
  • 9:40 - 10:20 终于把暴力写完了,发现没过样例,再看一看题,发现题意有点理解错。但是稍微改一下就好了。自测发现大样例T了。
  • 10:20 - 10:25 发现可以用单调队列优化成线性。开写,细节有点多。
  • 10:25 - 11:20 过了大样例,尝试对拍,但是数据一直造的不好。过了看T4。
  • 11:20 - 12:00 写了40暴力,正解没想出来

得分:100 + 100 + 100 + 40 = 340
rk 6

题解

A. 江桥的必胜策略

原题连接

在这里插入图片描述
分析:

水题。发现 n , m , k n, m, k n,m,k 很小。将 a , b a,b ab 序列从小到大排序。 设 d p i , j , k , 0 / 1 , 0 / 1 dp_{i, j, k, 0/1, 0/1} dpi,j,k,0/1,0/1 表示考虑到 a a a 的第 i i i 个位置, b b b 的第 j j j 个位置,已经选中了 k k k 张牌, a i a_i ai 不选/选, b j b_j bj 不选/选的方案数。转移是简单的。但是开 l o n g   l o n g long \ long long long 会 MLE, 但是前两维可以滚动。

时间复杂度: O ( n × m × k ) O(n \times m \times k) O(n×m×k)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
const int M = 11;
typedef long long LL;
const int mod = 1000000009;
int n, m, K, a[N], b[N];
int dp[N][N][M][2][2];
int Mod(int x) {
	return x >= mod ? x - mod : x;
}
int main() {
	scanf("%d%d%d", &n, &m, &K);
	for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	for(int i = 1; i <= m; i ++ ) scanf("%d", &b[i]);
	sort(a + 1, a + n + 1); sort(b + 1, b + m + 1);
	for(int i = 0; i <= m; i ++ ) dp[0][i][0][0][0] = 1;
	for(int i = 0; i <= n; i ++ ) dp[i][0][0][0][0] = 1;
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j <= m; j ++ ) {
			for(int k = 0; k <= min({i, j, K}); k ++ ) {
		        dp[i][j][k][0][0] = Mod(Mod(Mod(dp[i - 1][j - 1][k][0][0] + dp[i - 1][j - 1][k][0][1]) + dp[i - 1][j - 1][k][1][0]) + dp[i - 1][j - 1][k][1][1]);
		        dp[i][j][k][0][1] = Mod(dp[i - 1][j][k][1][1] + dp[i - 1][j][k][0][1]);
		        dp[i][j][k][1][0] = Mod(dp[i][j - 1][k][1][0] + dp[i][j - 1][k][1][1]);
		        if(a[i] > b[j] && k) dp[i][j][k][1][1] = Mod(Mod(Mod(dp[i - 1][j - 1][k - 1][0][0] + dp[i - 1][j - 1][k - 1][0][1]) + dp[i - 1][j - 1][k - 1][1][0]) + dp[i - 1][j - 1][k - 1][1][1]);
			}
		}
	}
	printf("%d\n", Mod(Mod(Mod(dp[n][m][K][0][0] + dp[n][m][K][0][1]) + dp[n][m][K][1][0]) + dp[n][m][K][1][1]));
	return 0;
}

B. 江桥的灌溉

原题链接

在这里插入图片描述

分析:

实际上是给你 n n n 条线段,你需要将整条链划分成若干段,使得每条线段都被一段包含,并且每段的长度是在 [ 2 X , 2 Y ] [2X, 2Y] [2X,2Y] 之间的偶数。问最小的段数。

我们考虑朴素 d p dp dp:设 d p i dp_i dpi 表示前 i i i 个点进行划分,满足 所有右端点小于等于 i i i 的线段被一段包含 的最小段数。那么转移就是 d p i ← d p j dp_i \gets dp_j dpidpj j j j 需要满足 没有一条线段的端点在 j j j 两侧 i − j ∈ [ 2 X , 2 Y ] i - j \in [2X, 2Y] ij[2X,2Y] 且是偶数。 暴力枚举复杂度 O ( L 2 ) O(L^2) O(L2)

不难发现随着 i i i 的增大,原来 因被线段包含而不能转移 的点仍然不能转移。因此我们可以把每条线段存到它的右端点里,每次到一个 i i i 就枚举它 v e c t o r vector vector 里的线段,把原来的一些被线段覆盖的转移点删去。查询要查一个区间里跟当前位置奇偶性相同的位置上的最小值。要支持 单点修改,区间查询。可以用线段树维护。复杂度 O ( L × l o g 2 L ) O(L \times log_2L) O(L×log2L)

但是这个题卡 l o g log log。我们发现每次转移的区间长度是固定的,这相当于一个区间在滑动。想到 单调队列。我们考虑维护两个单调队列 q j , q o qj,qo qjqo,分别维护奇数位置和偶数位置。对于当前点 i i i,我们插入 i − 2 X i - 2X i2X 这个决策点,然后维护区间长度时弹出队首。转移时从队首取决策点即可。 注意: 这里我们不能在进行每次枚举右端点为 i i i 的线段弹出队尾,因为队尾可能已经把一些合法的转移点弹出队列。但是注意到 至少存在一条线段的左右端点在其两边的点 x x x 一定不会成为最终答案的一段的端点,因此我们可以先标记出那些点是作为转移点是不合法的,然后插入 i − 2 X i - 2X i2X 时先判断,如果合法再插入即可。

时间复杂度 O ( L ) O(L) O(L)

CODE:

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1e6 + 10;
const int INF = 1e8;
int n, l, x, y, S, T, dp[N];
int cnt[N];
bool vis[N];
vector< int > lt[N];
deque< int > qj, qo; // 单调队列优化 
int main() {
	scanf("%d%d", &n, &l);
	scanf("%d%d", &x, &y);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d%d", &S, &T);
		if(T > S + 1) cnt[S + 1] ++, cnt[T] --;
	}
	for(int i = 0; i <= l; i ++ ) {
		if(i != 0) cnt[i] += cnt[i - 1];
		if(cnt[i] > 0) vis[i] = 1;
	}
	for(int i = 0; i <= l; i ++ ) {
		if(i == 0) dp[i] = 0;
		else {
			if(i - 2 * x >= 0) {
				int p = i - 2 * x;
				if(!vis[p]) { // 能够成为决策点 
					if(i & 1) {
						while(!qj.empty() && (dp[p] <= dp[qj.back()])) qj.pop_back();
						qj.push_back(p);
					}
					else {
						while(!qo.empty() && (dp[p] <= dp[qo.back()])) qo.pop_back();
						qo.push_back(p);
					}
				}
			}
			while(!qj.empty() && (i - qj.front() > 2 * y)) {
				qj.pop_front();
			}
			while(!qo.empty() && (i - qo.front() > 2 * y)) {
				qo.pop_front();
			} 
			if(i & 1) { // 放 
				if(qj.empty()) dp[i] = INF;
				else {
					int u = qj.front();
			    	dp[i] = dp[u] + 1;
				}
			}
			else {
				if(qo.empty()) dp[i] = INF;
				else {
					int u = qo.front();
					dp[i] = dp[u] + 1;
				}
			}
		}
	}
	if(dp[l] > l + 10) puts("-1");
	else printf("%d\n", dp[l]); 
	return 0;
}

C. 江桥的书架

原题链接

在这里插入图片描述

分析:

实际上是给你 n n n 个元素,每个元素有两种数值 h i h_i hi w i w_i wi。你需要将序列划分为若干段,需要保证每一段的 w i w_i wi 之和小于等于 L L L,一段的权值为区间最大的 h i h_i hi。求最小的划分权值和。

好像一段的价值函数满足 反四边形不等式(包含优于交叉),因此dp转移满足决策单调性。但是一个 d p i dp_i dpi 的转移点是一段区间。直接把每个位置插入它的转移点区间对应的线段树上 l o g 2 n log_2n log2n 个节点上,然后按照 左 - 右 - 根 的顺序在线段树的每个节点上分治转移即可。复杂度 O ( l o g 2 2 n ) O(log_2^2n) O(log22n)

但是可以单 l o g log log

发现如果 h i h_i hi 单调不减,那么每个 i i i 显然是找到最前面满足 ∑ k = j i w k ≤ L \sum_{k = j}^{i} w_k \leq L k=jiwkL j j j 转移即可。这是因为 j j j 靠前 d p j dp_j dpj 越小,但是 j ∼ i j \sim i ji 这一段的权值仍然是 h i h_i hi。 这启示我们 [ 1 , i ] [1, i] [1,i]的后缀最大值 [ 1 , i ] [1, i] [1,i] 划分为若干段,那么左端点 j j j 在同一段内时 [ j , i ] [j, i] [j,i] 区间的权值相同,所以用这一段最左边的转移。

维护 [ 1 , i ] [1, i] [1,i] 的后缀最大值可以用 单调栈。我们考虑用 线段树 在每个后缀最大值的位置插入以它为上一段的右端点时的转移值。然后在 弹栈和入栈 时修改线段树中对应的转移值,查询就是通过 二分 找到最左边的 j j j,然后在线段树上查一段区间的最小值。注意还要将 j j j 作为决策点转移一次。这是要考虑没有覆盖完一段的情况。

时间复杂度 O ( n × l o g 2 n ) O(n \times log_2n) O(n×log2n)

CODE:

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
const LL INF = 1e15;
int n;
LL L, h[N], w[N], sum[N], dp[N];
LL mx[N][21];
LL query(int l, int r) {
	int k = log2(r - l + 1);
	return max(mx[l][k], mx[r - (1 << k) + 1][k]);
}
void build_st() {
	for(int i = 1; i <= n; i ++ ) mx[i][0] = h[i];
	for(int i = 1; (1 << i) <= n; i ++ ) {
		for(int j = 1; j + (1 << i) - 1 <= n; j ++ ) {
			mx[j][i] = max(mx[j][i - 1], mx[j + (1 << (i - 1))][i - 1]);
		}
	}
}
void solve1() { // n^2
	memset(dp, 0x3f, sizeof dp);
	dp[0] = 0;
	for(int i = 1; i <= n; i ++ ) {
		LL maxh = 0, nw = 0;
		for(int j = i; j >= 1; j -- ) {
			maxh = max(maxh, h[j]); nw += w[j];
			if(nw > L) break;
			dp[i] = min(dp[i], dp[j - 1] + maxh);
		}
	}
	printf("%lld\n", dp[n]);
}
void solve2() {
	memset(dp, 0x3f, sizeof dp); dp[0] = 0;
	vector< LL > vec; vec.pb(0);
	for(int i = 1; i <= n; i ++ ) {
		if(sum[i] <= L) dp[i] = h[i];
		else {
			int idx = lower_bound(vec.begin(), vec.end(), sum[i] - L) - (vec.begin());
			dp[i] = dp[idx] + h[i];
		}
		vec.pb(sum[i]);
	}
	printf("%lld\n", dp[n]);
}
struct SegmentTree {
	int l, r;
	LL val;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define val(x) t[x].val
}t[N * 4];
void update(int p) {
	val(p) = min(val(p << 1), val(p << 1 | 1));
}
void build(int p, int l, int r) {
	l(p) = l, r(p) = r;
	if(l == r) {val(p) = INF; return ;}
	int mid = (l + r >> 1);
	build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
	update(p);
}
void change(int p, int pos, LL c) {
	if(l(p) == r(p)) {
		val(p) = c;
		return ;
	}
	int mid = (l(p) + r(p) >> 1);
	if(pos <= mid) change(p << 1, pos, c);
	else change(p << 1 | 1, pos, c);
	update(p);
}
LL ask(int p, int l, int r) {
	if(l <= l(p) && r >= r(p)) return val(p);
	int mid = (l(p) + r(p) >> 1);
	if(r <= mid) return ask(p << 1, l, r);
	else if(l > mid) return ask(p << 1 | 1, l, r);
	else return min(ask(p << 1, l, r), ask(p << 1 | 1, l, r));
}
stack< int > s;
void solve3() {
	memset(dp, 0x3f, sizeof dp); dp[0] = 0;
	build(1, 1, n); 
	vector< LL > vec; vec.pb(0);
	for(int i = 1; i <= n; i ++ ) {
		while(!s.empty() && h[i] >= h[s.top()]) {
			int x = s.top();
			change(1, x, INF);
			s.pop();
		}
		if(!s.empty()) {
			int x = s.top();
			change(1, x, dp[x] + h[i]);
		}
		s.push(i);
		change(1, i, INF);
		if(sum[i] <= L) dp[i] = query(1, i);
		else {
			int idx = lower_bound(vec.begin(), vec.end(), sum[i] - L) - (vec.begin());
			dp[i] = min(dp[i], dp[idx] + query(idx + 1, i));
			LL c = ask(1, idx, i);
			dp[i] = min(dp[i], c);
		}
		vec.pb(sum[i]);
	}
	printf("%lld\n", dp[n]);
}
int main() {
	scanf("%d%lld", &n, &L);
	bool flag = 1;
	for(int i = 1; i <= n; i ++ ) {
		scanf("%lld%lld", &h[i], &w[i]);
		if(h[i] < h[i - 1]) flag = 0;
		sum[i] = sum[i - 1] + w[i];
	}
	build_st();
	if(n <= 1000) solve1(); // 40
	else if(flag) solve2(); // 20
	else solve3(); // 40
	return 0;
}

D. 江桥的疑惑

原题连接

在这里插入图片描述

分析:

感觉这题真的没黑啊。

我们先考虑暴力。显然有一个广搜:记状态 v i s p x , p y , b x , b y vis_{px, py, bx, by} vispx,py,bx,by 表示人在 ( p x , p y ) (px, py) (px,py),箱子在 ( b x , b y ) (bx, by) (bx,by) 的状态是否可行。然后标记初始状态跑广搜即可。时空复杂度 O ( N M × N M ) O(NM \times NM) O(NM×NM)

我们发现很多状态是无用的。更具体的:人推箱子走的几步是关键的。我们只关心对于箱子的位置 ( b x , b y ) (bx, by) (bx,by),人能不能 不经过箱子 移动到箱子的上下左右位置。

于是我们设 v i s i , j , F vis_{i, j, F} visi,j,F 表示箱子在 ( i , j ) (i, j) (i,j) 位置,人在它的 F F F 方向,与它相邻的状态是否可行。考虑如何拓展:首先可以推箱子,这个是简单的。关键是能否 从一个方向移动到另一个方向

对于一个方向 F F F 和另一个方向 F ′ F' F,显然可以通过箱子的位置由 F F F F ′ F' F。这是一条路径,由于我们不能经过箱子,因此还需另一条不经过 箱子的路径。我们发现这等价于 两个位置都在同一个点双中

因此我们处理出来每个位置所属的点双。拓展判断暴力枚举两个点所属的点双编号看有没有交。

简单说一下暴力枚举点双编号为什么是对的:

  1. 两个点被判断当且仅当队首状态的位置与这两个点都相邻,一个点对最多被判断 4 4 4 次。那么一个点的点双最多被枚举 32 32 32
  2. 一条边最多属于一个点双,每个点最多通过每一条边分到一个点双中,因此所有点所属的点双数量之和为 O ( n m ) O(nm) O(nm) 级别。
  3. 因此复杂度最多是 1500 × 1500 × 32 = 72000000 1500 \times 1500 \times 32 = 72000000 1500×1500×32=72000000,但实际上严重跑不满。

最后每次询问 O ( 1 ) O(1) O(1) 判断一下即可。

时间复杂度 O ( n × m ) O(n \times m) O(n×m)

CODE:

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1505;
int n, m, Q, px, py, bx, by, rk, bin[N * N];
int dfn[N * N], low[N * N], cnt;
int dx[5] = {0, -1, 0, 0, 1}, dy[5] = {0, 0, 1, -1, 0};
char mp[N][N];
bool vis[N][N][4];
vector< int > bel[N * N]; // 每个点所属点双的编号 
int ID(int x, int y) {
	return (x - 1) * m + y;
}
vector< int > E[N * N];
void add(int x, int y) {E[x].pb(y);}
struct state {
	int x, y, F;
};
queue< state > q;
stack< int > scc;
void tarjan(int x) {
	low[x] = dfn[x] = ++ rk; scc.push(x);
	for(auto v : E[x]) {
		if(!dfn[v]) {
			tarjan(v);
			low[x] = min(low[x], low[v]);
			if(low[v] >= dfn[x]) { // 点双 
			    cnt ++;
				for(int u = 0; u != v; ) {
					u = scc.top(); scc.pop();
					bel[u].pb(cnt);
				}
				bel[x].pb(cnt);
			}
		}
		else low[x] = min(low[x], dfn[v]);
	}
}
bool bok[N * N * 4];
bool check(int px, int py, int qx, int qy) {
	for(int b : bel[ID(px, py)]) bok[b] = 1;
	bool flag = 0;
	for(int b : bel[ID(qx, qy)]) flag |= bok[b];
	for(int b : bel[ID(px, py)]) bok[b] = 0;
	return flag;
}
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
int main() {
	scanf("%d%d%d", &n, &m, &Q);
	for(int i = 1; i <= n * m; i ++ ) bin[i] = i;
	for(int i = 1; i <= n; i ++ ) {
		scanf("%s", mp[i] + 1);
	}
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j <= m; j ++ ) {
			if(mp[i][j] == 'A') px = i, py = j;
			else if(mp[i][j] == 'B') bx = i, by = j;
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j <= m; j ++ ) {
			if(mp[i][j] == '#') continue;
			for(int k = 1; k <= 4; k ++ ) {
				int tx = i + dx[k], ty = j + dy[k];
				if(tx >= 1 && tx <= n && ty >= 1 && ty <= m && mp[tx][ty] != '#') {
					if(mp[i][j] != 'B' && mp[tx][ty] != 'B') {
						int f1 = Find(ID(i, j)), f2 = Find(ID(tx, ty));
                        if(f1 != f2) bin[f1] = f2;
					}
					add(ID(i, j), ID(tx, ty)); // 连一条边 
				}
			}
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j <= m; j ++ ) {
			if(!dfn[ID(i, j)]) {
				stack< int > tmp; swap(tmp, scc);
				tarjan(ID(i, j));
			}
		}
	}
	for(int i = 1; i <= 4; i ++ ) {
		int rx = bx + dx[i], ry = by + dy[i];
		if(rx >= 1 && rx <= n && ry >= 1 && ry <= m && mp[rx][ry] != '#') {
			if(Find(ID(px, py)) == Find(ID(rx, ry))) {
				q.push((state) {bx, by, i}); // i 表示方向 
				vis[bx][by][i] = 1;
			}
		}
	}
	while(!q.empty()) {
		state msk = q.front(); q.pop();
		int nx = msk.x, ny = msk.y, F = msk.F;
		// 沿原来方向推 
		int tx = nx + dx[5 - F], ty = ny + dy[5 - F];
		if(tx >= 1 && tx <= n && ty >= 1 && ty <= m && mp[tx][ty] != '#') {
			if(!vis[tx][ty][F]) {
				vis[tx][ty][F] = 1;
				q.push((state) {tx, ty, F});
			}
		}
		int ux = nx + dx[F], uy = ny + dy[F];
		for(int i = 1; i <= 4; i ++ ) {
			if(i != F) {
				int ox = nx + dx[i], oy = ny + dy[i];
				if(ox >= 1 && ox <= n && oy >= 1 && oy <= m && mp[ox][oy] != '#') {
					if(check(ux, uy, ox, oy)) {
						if(!vis[nx][ny][i]) {
							vis[nx][ny][i] = 1;
							q.push((state) {nx, ny, i});
						}
					}
				}
			}
		}
	}
	for(int i = 1; i <= Q; i ++ ) {
		int gx, gy; scanf("%d%d", &gx, &gy);
		bool f = 0;
		for(int j = 1; j <= 4; j ++ ) f |= vis[gx][gy][j];
		if(f || (bx == gx && by == gy)) puts("YES");
		else puts("NO");
	}
	return 0;
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值