2024 信友队 noip 冲刺 8.31

T1

n n n 个只有时针的时钟,初始时针指向 3 , 6 , 9 , 12 3,6,9,12 3,6,9,12 点钟方向中的一个。每次操作可以选择一段编号连续的时钟使时针顺时针旋转 9 0 ∘ 90^\circ 90,给定每个时钟时针的目标方向求最小操作数量。

n ≤ 1 0 5 n\le 10^5 n105

赛时打了个看起来很假的 O ( n 2 ) O(n^2) O(n2) dp 拿了 40 p t s 40\mathrm{pts} 40pts:令 f ( i ) f(i) f(i) 表示使 [ 1 , i ] [1,i] [1,i]​​ 合法的最小操作数量,转移时考虑把之前某个操作延长到自己使自己可以少操作几步,对于中间的部分直接按照不考虑时针转过头的情况计算最小步数。一开始打的暴搜根本跑不出来。

namespace Subtask1 {
	const int maxn = 1e5 + 5;
	int a[maxn], b[maxn], f[maxn], n;
	int main() {
		scanf("%d", &n); 
		for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
		for (int i = 1; i <= n; i ++) scanf("%d", &b[i]), b[i] = (b[i] - a[i] + 4) % 4;
		f[1] = b[1];
		for (int i = 2; i <= n; i ++) {
			f[i] = f[i - 1] + max(0, b[i] - b[i - 1]);
			int mor = 0, lst = 0;
			for (int j = i - 1; j >= 2; j --) {
				int now = (b[j] - b[i] + 4) % 4;
				mor += max(0, now - lst), lst = now;
				f[i] = min(f[i], f[j - 1] + mor + max(0, b[i] - b[j - 1]));
			}
			// cout << i << ' ' << f[i] << '\n';
		} printf("%d\n", f[n]);
		return 0;
	}
}

同机房 AC 的非常玄学,挂的也非常玄学,令 f ( i , j ) f(i,j) f(i,j) 表示先使第 i i i 个钟转了 j j j 次,然后再考虑合法的最小操作数,玄学的地方在于他们令 j ∈ [ 0 , 100 ] j\in[0,100] j[0,100] 非常骗分,对于每个 i i i 枚举时针转几周,然后从 f ( i − 1 , k ) f(i-1,k) f(i1,k) 转移,玄学的地方也在于他们令 k ∈ [ j − w , j + w ] k\in [j-w,j+w] k[jw,j+w](其中 w w w 为一个个位数)……就很难评价,转移和我的想法差不多,也是考虑从前一个的操作中进行扩展,使自己少操作一些。总之非常玄学。有几个人挂就因为这几个区间没调好挂了了 10 ∼ 30 p t s 10\sim 30\mathrm{pts} 1030pts。这玩意可以构造类似于 01230123 ⋯ 0 32103210 ⋯ 3210 01230123\cdots {\color{orange}0}32103210\cdots 3210 012301230321032103210 使得中间橙色的 0 0 0 转非常多圈。

正解:先考虑不   m o d   4 \bmod 4 mod4 的情况,那么答案就是 ∑ max ⁡ { a i − a i − 1 , 0 } \sum\max\{a_i-a_{i-1},0\} max{aiai1,0},证明的话考虑差分数组,对于 a i > a i − 1 a_i>a_{i-1} ai>ai1 那么显然需要在 a i a_i ai 处作为左端点新增一个加一的区间;对于 a i < a i − 1 a_i<a_{i-1} ai<ai1 那么会有一个区间在 a i − 1 a_{i-1} ai1 处作为右端点结束。考虑扩展到   m o d   4 \bmod 4 mod4 的情况,那我们肯定是让若干个 a i ← a i + 4 a_i\gets a_i+4 aiai+4 变为前一种情况。我们不妨令 b i = a i − a i − 1 b_i=a_i-a_{i-1} bi=aiai1,显然 b i ∈ [ − 3 , 3 ] b_i\in[-3,3] bi[3,3],但是负数在求和时会被视为为 0 0 0。考虑 + 4 +4 +4 的影响,若 a i ← a i + 4 a_i\gets a_i+4 aiai+4 那么 b i ← b i + 4 b_i\gets b_i+4 bibi+4 b i + 1 ← b i + 1 − 4 b_{i+1}\gets b_{i+1}-4 bi+1bi+14,扩展一下我们可以“花费”一个 + 4 +4 +4 并选择一个位置 − 4 -4 4,在 a i a_i ai 上就表现为区间加一个 4 4 4

我们贪心地将所有 b i < 0 b_i<0 bi<0 的数拿来 + 4 +4 +4(观察到 b i = − 1 b_i=-1 bi=1 时这么做是无意义的可以忽略),然后把正数 − 4 -4 4(同理 b i = 1 b_i=1 bi=1 无意义)。我们把所有 − 2 , − 3 -2,-3 2,3 存起来,遇到一个 2 2 2 我们考虑拿一个 − 3 -3 3 使得答案减一(使用 − 2 -2 2 相当于没用,不如存着),遇到一个 3 3 3 我们拿一个 − 2 / − 3 -2/-3 2/3 使得答案减一/建二,优先使用 − 3 -3 3。然后就做完了,很奇妙的贪心方法。赛时想到给若干项加 4 4 4 但没有把不   m o d   4 \bmod 4 mod4 的情况抽象成若干个 b i b_i bi​ 的变化。代码挺好写的就不放了。

T2

n n n 根柱子,第 i i i 根柱子高度为 H i H_i Hi H H H 1 ∼ n 1\sim n 1n 的排列;定义高跳表示从 i i i 柱子跳到一个 j j j 柱子满足 i < j , h i < h j i<j,h_i<h_j i<j,hi<hj,定义低跳表示从 i i i 柱子跳到一个 j j j 柱子满足 i < j , h i > h j i<j,h_i>h_j i<j,hi>hj。要求选定一个起点,第一次高跳低跳皆可,满足相邻两次跳跃的方式不同,求最多的跳跃次数和达到这个跳跃次数的路径数。

n ≤ 3 × 1 0 5 n\le 3\times 10^5 n3×105

一开始想的是建分层图,两层分别表示高跳过来 / 低跳过来,然后 spfa 跑最长路的同时维护方案数。赛后看提交记录发现 20 p t s 20\mathrm{pts} 20pts

namespace Subtask0 {
	const int maxn = 1005;
	namespace Graph {
		struct Edge { int to, nxt; } e[(maxn * maxn) << 1];
		int head[maxn << 1], ecnt = 0;
		void addEdge(int u, int v) { e[++ ecnt] = Edge { v, head[u]}, head[u] = ecnt; }
	} using namespace Graph;
	const int P = 1e9 + 7;
	int n, H[maxn]; queue<int> q; int f[maxn], g[maxn];
	bool vis[maxn];
	pair<int, int> SPFA() {
		for (int i = 1; i <= (n << 1); i ++)
			f[i] = 1, g[i] = 1, vis[i] = 1, q.push(i);
		while (!q.empty()) {
			int u = q.front(); q.pop(); vis[u] = 0;
			// cout << u << ' ' << f[u] << ' ' << g[u] << '\n';
			for (int i = head[u], v; i; i = e[i].nxt) {
				if (f[v = e[i].to] < f[u] + 1) {
					// cout << "update:" << v << '\n';
					f[v] = f[u] + 1, g[v] = g[u];
					if (!vis[v]) vis[v] = 1, q.push(v);
				} else if (f[v] == f[u] + 1) g[v] = (1ll * g[v] + g[u]) % P;
			}
		}
		int ans1 = 0, ans2 = 1;
		for (int i = 1; i <= (n << 1); i ++)
			if (f[i] > ans1) ans1 = f[i], ans2 = g[i];
			else if (f[i] == ans1) ans2 = (1ll * ans2 + g[i]) % P;
		return { ans1, ans2 };
	}
	int main() {
		scanf("%d", &n);
		for (int i = 1; i <= n; i ++)
			scanf("%d", &H[i]);
		for (int i = 1; i <= n; i ++)
			for (int j = i + 1; j <= n; j ++)
				if (H[i] > H[j]) addEdge(i, j + n);
				else addEdge(i + n, j);
		auto [dis, way] = SPFA();
		return printf("%d %d\n", dis, way), 0;
	}
}

然后发现做一个简单 dp 即可做到 O ( n 2 ) O(n^2) O(n2),令 f ( i , 0 / 1 ) f(i,0/1) f(i,0/1) 表示从 i i i 出发下一步高跳 / 低跳的最长路径,再记一个 g ( i , 0 / 1 ) g(i,0/1) g(i,0/1) 维护方案数,转移很朴素。拿了 50 p t s 50\mathrm{pts} 50pts

namespace Subtask1 {
	const int maxn = 1005, P = 1e9 + 7;
	int f[maxn][2], g[maxn][2], H[maxn], n;
	int main() {
		scanf("%d", &n);
		for (int i = 1; i <= n; i ++) scanf("%d", &H[i]);
		f[n][0] = f[n][1] = 1, g[n][0] = g[n][1] = 1;
		for (int i = n - 1; i >= 1; i --)
			for (int j = i + 1; j <= n; j ++)
				if (H[i] > H[j]) {
					if (f[i][0] < f[j][1] + 1)
						f[i][0] = f[j][1] + 1, g[i][0] = g[j][1];
					else if (f[i][0] == f[j][1] + 1)	
						g[i][0] = (1ll * g[i][0] + g[j][1]) % P;
				} else {
					if (f[i][1] < f[j][0] + 1)
						f[i][1] = f[j][0] + 1, g[i][1] = g[j][0];
					else if (f[i][1] == f[j][0] + 1)	
						g[i][1] = (1ll * g[i][1] + g[j][0]) % P;
				}
		int dis = 0, way = 1;
		for (int i = 1; i <= n; i ++) {
			// cout << f[i][0] << ' ' << f[i][1] << ',' << g[i][0] << ' ' << g[i][1] << '\n';
			if (f[i][0] > dis) dis = f[i][0], way = g[i][0];
			else if (f[i][0] == dis) way = (1ll * way + g[i][0]) % P;
			if (f[i][1] > dis) dis = f[i][1], way = g[i][1];
			else if (f[i][1] == dis) way = (1ll * way + g[i][1]) % P;
		} printf("%d %d\n", dis, way);
		return 0;
	}
}

考虑优化时赛时认为要树套树,因为维护 f f f 后要在 g g g 中取最长路为 f f f 的值求和,然后成功被自己薄纱。优化十分简单,直接用两棵权值树状数组分别维护高跳和低跳的情况,前者可以把数组倒过来后缀变前缀;树状数组中的元素直接是最长路 + 方案数,合并时讨论两者最长路即可。时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn) 真给我糖丸了。

namespace ACCode {
	const int maxn = 3e5 + 5, P = 1e9 + 7;
	int n, h[maxn];
	struct Answer {
		int dis, way; Answer(int x = 0, int y = 0) { dis = x, way = y; }
		Answer operator+(const Answer &oth) {
			if (dis != oth.dis) return dis > oth.dis ? Answer(dis, way) : oth;
			return Answer(dis, (way + oth.way) % P);
		}
	};
	namespace BIT {
		#define lowbit(x) ((x) & (-(x)))
		struct bit {
			Answer b[maxn];
			void add(int pos, Answer x) {
				for (; pos <= n; pos += lowbit(pos))
					b[pos] = b[pos] + x;
			} 
			Answer query(int pos) {
				Answer res;
				for (; pos; pos -= lowbit(pos)) res = res + b[pos];
				return res;
			}
		} low, hig;
	} using namespace BIT;
	Answer f[maxn][2];
	int main() {
		scanf("%d", &n);
		for (int i = 1; i <= n; i ++) scanf("%d", &h[i]);
		for (int i = n; i; i --) {
			Answer up = hig.query(n - h[i] + 1), down = low.query(h[i] - 1);
			f[i][0] = Answer(up.dis + 1, up.way == 0 ? 1 : up.way), f[i][1] = Answer(down.dis + 1, down.way == 0 ? 1 : down.way);
			// cout << f[i][0].dis << ' ' << f[i][0].way << " " << f[i][1].dis << ' ' << f[i][1].way << '\n';
			hig.add(n - h[i] + 1, f[i][1]), low.add(h[i], f[i][0]);
		} Answer ans;
		for (int i = 1; i <= n; i ++) for (int j = 0; j < 2; j ++)
			ans = ans + f[i][j];
		return printf("%d %d", ans.dis, ans.way), 0;
	}
}

T3

赛时前两题部分分写爽了看这题时没仔细看数据范围导致错过 20 p t s 20\mathrm{pts} 20pts 特殊性质,实际上一分没拿。 m = 0 m=0 m=0 时可以发现答案即为卡特兰数第 n n n 项, n ≤ 20 n\le 20 n20 考虑状压 dp 即可。这里放一下特殊性质 20 p t s 20\mathrm{pts} 20pts​。

namespace SubtaskSpecial {
	const int P = 1e9 + 7, maxn = 405;
	namespace Binom {
		int fac[maxn << 1], inv[maxn << 1];
		int qp(int x, int y) {
			int res = 1;
			for (; y; y >>= 1, x = 1ll * x * x % P)
				if (y & 1) res = 1ll * res * x % P;
			return res;
		}
		void init() {
			fac[0] = fac[1] = 1;
			for (int i = 2; i < maxn; i ++) fac[i] = 1ll * fac[i - 1] * i % P;
			inv[maxn - 1] = qp(fac[maxn - 1], P - 2);
			for (int i = maxn - 2; i >= 0; i --) inv[i] = 1ll * inv[i + 1] * (i + 1) % P;
		}
		int C(int x, int y) {
			if (x < y) return 0;
			return 1ll * fac[x] * inv[y] % P * inv[x - y] % P;
		}
	} using namespace Binom;
    // 直接用通项公式即可
	int Cat(int x) { return (C(x << 1, x) - C(x << 1, x - 1) + P) % P; }
	void solve(int n) { printf("%d\n", Cat(n)); }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值