2024 信友队 noip 冲刺 9.28

T1

给定一个长度为 n n n 的数组 a a a,需要选择若干段不相交子串,使得每个子串的和相等。求最多能选取多少个子串。

n ≤ 1000 n\le 1000 n1000 1 ≤ a i ≤ 20 1\le a_i\le20 1ai20

发现 ∑ a \sum a a 很小可以直接枚举每个子串的和,然后扫一遍即可。赛时用的队列贪心的选取,差点因为选取了相交子串而 20 p t s 20\mathrm{pts} 20pts​。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 1005;
int a[maxn], n, sum;
int q[maxn], l, r, ans, tot, now;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i ++) scanf("%d", &a[i]), sum += a[i];
	for (int s = 1; s <= sum; s ++) {
		now = 0, l = 1, r = tot = 0; 
		for (int i = 1; i <= n; i ++) {
			for (now += (q[++ r] = a[i]); l <= r && now > s; )
				now -= q[l ++];
			if (now == s) tot ++, l = r + 1, now = 0;
		} ans = max(ans, tot);
	} printf("%d\n", ans);
	return 0;
}

T2

n n n 个套装,第 i i i 个套装有 c i c_i ci 个物品,这些物品有参数 w i w_i wi,选取这个套装代价为 v i v_i vi;还有 m m m 个任务,完成第 i i i 个任务需要 w w w 至少为 W i W_i Wi 的物品 C i C_i Ci 个,完成后获得 V i V_i Vi 的收益。求最大收益减代价。

n , m ≤ 2000 n,m\le 2000 n,m2000 1 ≤ c i , C i ≤ 50 1\le c_i,C_i\le 50 1ci,Ci50 1 ≤ w i , W i , v i , V i ≤ 1 0 9 1\le w_i,W_i,v_i,V_i\le 10^9 1wi,Wi,vi,Vi109

套路:把买和卖放到一起做,对于买的情况价值取负。考虑 dp,令 f ( i ) f(i) f(i) 表示手上有 i i i 个物品时的最大收益。转移时滚动数组即可。对于 w w w 的限制,所有信息按照 w w w 从大到小做就行了。但是排序的时候没有考虑 w w w W W W 相等的情况,显然应当先考虑买后考虑卖,痛失 16 p t s 16\mathrm{pts} 16pts​。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 2005, maxc = 1e5 + 5;
int n, m; 
struct Object { 
	int c, w, v; 
	inline bool operator<(const Object &oth) const {
		return w == oth.w ? v < oth.v : w > oth.w;
	}
} a[maxn << 1];
ll f[maxc]; const ll inf = 1e18;
inline int read() {
	char c = getchar();
	for (; c < '0' || c > '9'; c = getchar());
	int s = 0;
	for (; c >= '0' && c <= '9'; c = getchar())
		s = (s << 1) + (s << 3) + (c ^ 48);
	return s;
}
int main() {
	n = read(), m = read(); int tmp = 0;
	for (int i = 1; i <= n; i ++) a[i].c = read(), a[i].w = read(), a[i].v = -read(), tmp += a[i].c;
	for (int i = n + 1; i <= n + m; i ++) a[i].c = read(), a[i].w = read(), a[i].v = read();
	n += m, m = tmp; sort(a + 1, a + n + 1); ll ans = 0;
	for (int i = 1; i <= m; i ++) f[i] = -inf;
	for (int i = 1; i <= n; i ++) {
		int c = a[i].c, v = a[i].v;
		if (v < 0) for (int j = m; j >= c; j --) {
			if (f[j - c] == -inf) continue; 
			ans = max(ans, f[j] = max(f[j], f[j - c] + v));
			// cout << j << ' ' << f[j] << '\n';
		} else for (int j = 0; j + c <= m; j ++) {
			if (f[j + c] == -inf) continue; 
			ans = max(ans, f[j] = max(f[j], f[j + c] + v));
			// cout << j << ' ' << f[j] << '\n';
		}
	} printf("%lld\n", ans);
	return 0;
}

T3

给定一张 n n n 个点 m m m 条边的无向图,每条边有值 0 0 0 1 1 1。给定 d d d,对于所有起点为 1 1 1、长度为 d d d 的路径(可以走重复的点和边),将路径上经过的边的值依次连接可以组成若干 d d d 位的二进制数。求这些二进制数的种类数。

n ≤ 90 n\le 90 n90 m ≤ n ( n − 1 ) m\le n(n-1) mn(n1) 1 ≤ d ≤ 20 1\le d\le 20 1d20。有重边和自环。

看起来就很像有关 2 d 2^d 2d 的复杂度的做法,但是没有想出来,最后写了一个 30 p t s 30\mathrm{pts} 30pts 的暴搜,如果和 cout << (1 << d) 拼起来大概能拿 40 p t s 40\mathrm{pts} 40pts 左右,但是忘了这么做。正解是考虑 meet-in-the-middle,告诉算法后也就会做了。记 f ( i , j , k ) f(i,j,k) f(i,j,k) g ( i , j , k ) g(i,j,k) g(i,j,k) 表示到了 i i i 点已经走了 j j j 步是否可以有 k k k 这个二进制,其中 j ≤ 10 j\le 10 j10。前者起点为 1 1 1 后者以所有点为起点,然后 O ( n 2 d ) O(n2^d) O(n2d) 合并一下即可。快把这个算法忘了。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 95, maxm = maxn * maxn;
namespace Graph {
	struct Edge { int to, nxt, v; } e[maxm << 1];
	int head[maxn], ecnt;
	void addEdge(int u, int v, int c) {
		e[++ ecnt] = Edge { v, head[u], c }, head[u] = ecnt;
	}
} using namespace Graph;
const int maxd = 15;
bool f[maxn][maxd][1 << 10], g[maxn][maxd][1 << 10]; int n, m, d;
void dfs1(int u, int now, int step) {
	if (f[u][step][now]) return;
	f[u][step][now] = 1;
	if (step == (d >> 1)) return ;
	for (int i = head[u]; i; i = e[i].nxt)
		dfs1(e[i].to, now << 1 | e[i].v, step + 1);
}
void dfs2(int u, int now, int step) {
	if (g[u][step][now]) return;
	g[u][step][now] = 1;
	if (step == (d >> 1) + (d & 1)) return ;
	for (int i = head[u]; i; i = e[i].nxt)
		dfs2(e[i].to, now | (e[i].v << step), step + 1);
}
int main() {
	scanf("%d %d %d", &n, &m, &d);
	for (int i = 1, u, v, c; i <= m; i ++)
		scanf("%d %d %d", &u, &v, &c), addEdge(u, v, c), addEdge(v, u, c);
	dfs1(1, 0, 0); for (int i = 1; i <= n; i ++) dfs2(i, 0, 0);
	int ans = 0; for (int s = 0; s < (1 << d); s ++) {
		// cout << "s = " << s << '\n';
		int t0 = (s >> ((d >> 1) + (d & 1)));
		int t1 = (s - (t0 << ((d >> 1) + (d & 1)))), tmp = 0;
		for (int i = 1; i <= n; i ++) {
			// cout << i << ' ' << t0 << ' ' << f[i][t0] << ',' << t1 << ' ' << g[i][t1] << '\n';
			tmp |= (f[i][d >> 1][t0] & g[i][(d >> 1) + (d & 1)][t1]);
		} 
		// cout << "result: " << tmp << '\n';
		ans += tmp;
	} printf("%d\n", ans);
	return 0;
}

T4

维护一个长度为 n n n 的序列 a a a,支持两种操作 Q Q Q 次:

  • 给定区间 [ l , r ] [l,r] [l,r],对于所有 i ∈ [ l , r ] i\in [l,r] i[l,r] a i ← a i 2 a_i\gets a_i^2 aiai2
  • 给定区间 [ l , r ] [l,r] [l,r],求 ∑ i = l r a i \sum_{i=l}^r a_i i=lrai

1 ≤ n , q ≤ 2 × 1 0 5 1\le n,q\le 2\times 10^5 1n,q2×105,结果对 p = 998244353 p=998244353 p=998244353 取模。

原题有一档 O ( n Q ) O(nQ) O(nQ) 的小数据分和两档特殊性质,分别是单点修改和单点询问。单点修改拿个树状数组维护一下即可。单点询问用一个差分树状数组维护每个点被平方了几次,假设 x x x 被平方了 k k k 次,那么它现在的值就是:
x 2 k   m o d   p x^{2^k}\bmod p x2kmodp
用一下扩展欧拉定理进行降幂,原式变为:
x ( 2 k   m o d   φ ( p ) ) + φ ( p )   m o d   p x^{(2^k\bmod\varphi(p))+\varphi(p)}\bmod p x(2kmodφ(p))+φ(p)modp
做两次快速幂即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
bool MemoryST;
const int N = 2e5 + 5, P = 998244353;
int n, q, a[N];
struct Queries { int op, l, r; } Q[N];
namespace Subtask0 {
	void work() {
		for (int qq = 1; qq <= q; qq ++) {
			int op = Q[qq].op, l = Q[qq].l, r = Q[qq].r;
			if (op == 1) 
				for (int i = l; i <= r; i ++)
					a[i] = 1ll * a[i] * a[i] % P;
			else {
				int sum = 0;
				for (int i = l; i <= r; i ++)
					sum = (sum + a[i]) % P;
				printf("%d\n", sum);
			}
		}
	}
}
namespace Subtask1 {
	const int maxn = 2e5 + 5;
	namespace BIT {
		int b[maxn];
		#define lowbit(x) ((x) & (-(x)))
		void add(int i, int x) {
			for (; i <= n; i += lowbit(i))
				b[i] = (b[i] + x) % P;
		}
		int query(int i) {
			int ans = 0;
			for (; i; i -= lowbit(i)) ans = (b[i] + ans) % P;
			return ans;
		}
	} using namespace BIT;
	void work() {
		for (int i = 1; i <= n; i ++) add(i, a[i]);
		for (int qq = 1; qq <= q; qq ++) {
			int op = Q[qq].op, l = Q[qq].l, r = Q[qq].r;
			if (op == 1) add(l, 1ll * a[l] * ((a[l] + P - 1) % P) % P), a[l] = 1ll * a[l] * a[l] % P;
			else printf("%d\n", (query(r) - query(l - 1) + P) % P);
		}
	}
}
namespace Subtask2 {
	const int maxn = 2e5 + 5, P0 = 998244352;
	namespace BIT {
		int b[maxn];
		#define lowbit(x) ((x) & (-(x)))
		void add(int i, int x) {
			for (; i <= n; i += lowbit(i))
				b[i] += x;
		}
		int query(int i) {
			int ans = 0;
			for (; i; i -= lowbit(i)) ans += b[i];
			return ans;
		}
	} using namespace BIT;
	int qp(int x, int y, int p) {
		int res = 1;
		for (; y; y >>= 1, x = (1ll * x * x) % p)
			if (y & 1) res = 1ll * res * x % p;
		return res;
	}
	void work() {
		// for (int i = 1; i <= n; i ++) add(i, a[i]);
		for (int qq = 1; qq <= q; qq ++) {
			int op = Q[qq].op, l = Q[qq].l, r = Q[qq].r;
			if (op == 1)
				add(l, 1), add(r + 1, -1);
			else {
				int tot = query(l), tmp = qp(2, tot, P0) + P0;
				printf("%d\n", qp(a[l], tmp, P));
			}
		}
	}
}
bool MemoryED;
int main() {
	scanf("%d %d", &n, &q);
	for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
	bool stag1 = 1, stag2 = 1;
	for (int i = 1; i <= q; i ++) {
		scanf("%d %d %d", &Q[i].op, &Q[i].l, &Q[i].r);
		if (Q[i].op == 1 && Q[i].l != Q[i].r) stag1 = 0;
		if (Q[i].op == 2 && Q[i].l != Q[i].r) stag2 = 0;
	} if (stag1) return Subtask1::work(), 0;
	if (stag2) return Subtask2::work(), 0;
	Subtask0::work();
	cerr << fixed << setprecision(6) << (&MemoryED - &MemoryST) / 1024.0 << "KB\n";
	cerr << 1e3 * clock() / CLOCKS_PER_SEC << "ms\n";
	return 0;
}

正解是观察到 φ ( p ) = 998244352 = 2 23 × 119 \varphi(p)=998244352=2^{23}\times 119 φ(p)=998244352=223×119 会出现循环节,然后就可以用线段树维护了,循环节长度可以打表发现是 24 24 24。时间复杂度就是 O ( n log ⁡ n ) O(n\log n) O(nlogn) 24 24 24 的常数,用来处理循环节。线段树上每个点记录一下是否进入循环节和循环节的数组即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值