Loading

NOI Online 2021 补题

P7468 [NOI Online 2021 提高组] 愤怒的小 N

P7469 [NOI Online 2021 提高组] 积木小赛

P7470 [NOI Online 2021 提高组] 岛屿探险

考场经历

开场看一遍题,认定 T2 是签到, T1 很可做并且做出来就可以拉开差距,T3 一脸不会,毒瘤DS。事实上大致是准的,只不过 T3 想难了。

准备光速写个 T2,T1 打完去 T3 骗分。

果然 T2 很快敲完。但是不知道为什么只敲了 70,脑抽了。后来才反应过来 100 非常简单,码掉之后开 T1。但是因为CCF爆炸没交上去/tuu

T1 随便分析一下就是一个裸的多项式平移,\(O(nk^2)\)。想了想决定先码掉,结果整场考试都在调这玩意,,,

游记结束。

T1 到底是怎么了呢?

我输出了 dp 值之后发现从第 \(k\) 项开始都相同,想着必然是我预处理dp那块挂了,死命调。

直到考试结束最后 5min,想着还是要打点暴力,于是写了个暴力,发现 \(n\) 只有一个 \(1\) 都是对的!

原来是我统计答案忘记多项式平移了!!/tuu

然后再用那个性质,从第 \(k\) 项开始相同,直接就切了/tuu

这 T1 不比你 T3 简单?怎么在洛谷 T1 是黑的,T3 是紫的???

多给我两小时我可以 210+,这不 rk 前十/xia。看样子即使是 OI 赛制,手速也是非常重要的!


T1 愤怒的小 N

\(a\)\(0\)\(b\)\(1\)

可以发现,长度为 \(2^i\) 的序列,右半边 \(1\) 就是左边 \(0\) 下标集体加 \(2^{i-1}\) 得到的,就是一个多项式平移(本质是二项式定理展开)。

考虑直接维护 \(a_i\) 的系数。

\(g(i,k,j)(k\in \{0,1\})\) 表示长度为 \(2^i\) 的序列,\(k\) 所在下标构成的多项式,\(a_j\) 的系数。

不妨把 \(g(i,k)\) 看做一个多项式,定义函数 \(\operatorname{TRANS}(F(x),p)\) 表示 \(F(x+p)\)

有转移:

\[g(i,k)=g(i-1,k)+\operatorname{TRANS}(g(i-1,k\bigoplus 1),2^{i-1}) \]

把后半部分展开。

注意到 \(g\) 其实是由很多项式加起来得到的,推的时候应该展开再合并。

假设有值的位置有 \(q\) 个,分别为 \(x_1,x_2,\cdots,x_q\)

那么 \(\operatorname{TRANS}\) 就是把

\[F(x)=\sum_{j=0}^{k-1}a_jg_j\\ =\sum_{i=1}^{q}\sum_{j=0}^{k-1}a_jx_i^j \]

变成

\[F(x+p)=\sum_{i=1}^{q}\sum_{j=0}^{k-1}a_j(x_i+p)^j\\ =\sum_{j=0}^{k-1}a_j\sum_{i=1}^{q}\sum_{l=0}^{j}x_i^{j-l}p^{l}\binom{j}{l}\\ =\sum_{j=0}^{k-1}a_j\sum_{l=0}^{j}g_{j-l}p^l\binom{j}{l} \]

可以 \(O(k^2)\) 转移。

统计答案的时候应该从高位往低位跑,维护一个 now 表示最高 \(i\) 位构成的二进制数的值,维护一个 op 表示当前 \(1\) 的个数。

可以发现答案是 \(\sum_{n_i=1}\operatorname{TRANS}(g(i,op\% 2),now)a_i\)

这就是一个非常简单的 \(O(nk^2)\) 解法,MTT 一下可以 \(O(nk\log k)\)

至于正解,就是很容易观察到 \(g(i,k)\)\(i\ge k\) 的时候,\(g(i,0)=g(i,1)\)

那么考虑 \(<k\) 的部分 \(O(k^3)\) 暴力 \(dp\)\(\ge k\) 的部分拉格朗日插值,求 \(f\) 的前缀和然后除以 \(2\) 即可。

组合数千万不要用阶乘 因为我一开始 \(nk^2\) 的时候用的阶乘懒得改,就被卡常了

发现被卡常了/tuu

翻了翻题解,发现可以用一个 ull 存,每 \(16\) 次取一次模,取模次数大大减少,就过去了。(感谢 _Enthalphy 的题解)。

复杂度 \(O(\log n+k^3)\)

Code
#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mkp(x,y) make_pair(x,y)
#define pb(x) push_back(x)
#define sz(v) (int)v.size()
typedef long long LL;
typedef double db;
template<class T>bool ckmax(T&x,T y){return x<y?x=y,1:0;}
template<class T>bool ckmin(T&x,T y){return x>y?x=y,1:0;}
#define rep(i,x,y) for(int i=x,i##end=y;i<=i##end;++i)
#define per(i,x,y) for(int i=x,i##end=y;i>=i##end;--i)
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
	while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
	return f?x:-x;
}

#define mod 1000000007
inline int qpow(int n, int k) {
	int res = 1;
	for(; k; k >>= 1, n = 1ll * n * n % mod)
		if(k & 1) res = 1ll * n * res % mod;
	return res;
}
inline void fmod(int &x) { x -= mod, x += x >> 31 & mod; }

const int N = 500005;
const int K = 505;
int n, k, a[K], pw2[N], ans, dp[K][2][K], C[K][K];
char S[N];
inline void init() {
	pw2[0] = 1;
	rep(i, 1, n) pw2[i] = (pw2[i - 1] << 1) % mod;
	C[0][0] = 1;
	rep(i, 1, k) {
		C[i][0] = 1;
		rep(j, 1, i) fmod(C[i][j] = C[i - 1][j] + C[i - 1][j - 1]);
	}
}
inline int lagrange(int X, int k, int *a) {
	static int x[K], y[K];
	for(int i = 0, s = 0; i <= k; ++i) {
		x[i] = i, y[i] = s;
		int tmp = 0;
		for(int j = 0, o = 1; j < k; ++j, o = 1ll * o * i % mod)
			fmod(tmp += 1ll * o * a[j] % mod);
		fmod(s += tmp);
	}
	int res = 0;
	for(int i = 0; i <= k; ++i) {
		int A = y[i], B = 1;
		for(int j = 0; j <= k; ++j) if(i != j)
			A = 1ll * A * (X - x[j] + mod) % mod,
			B = 1ll * B * (x[i] - x[j] + mod) % mod;
		fmod(res += 1ll * A * qpow(B, mod - 2) % mod);
	}
	return res;
}
signed main() {
	scanf("%s%d", S, &k), n = strlen(S), init();
	for(int i = 0; i < k; ++i) a[i] = read();
	dp[0][0][0] = 1;
	for(int i = 1; i < min(n, k); ++i) {
		for(int j = 0; j < k; ++j) {
			unsigned long long r0 = 0, r1 = 0;
			for(int l = 0, o = 1; l <= j; ++l, o = 1ll * o * pw2[i - 1] % mod) {
				int t = 1ll * C[j][l]* o % mod;
				r0 += 1ull * dp[i - 1][0][j - l] * t;
				r1 += 1ull * dp[i - 1][1][j - l] * t;
				if(!(l & 15)) r0 %= mod, r1 %= mod;
			}
			dp[i][0][j] = (r1 + dp[i - 1][0][j]) % mod;
			dp[i][1][j] = (r0 + dp[i - 1][1][j]) % mod;
		}
	}
	reverse(S, S + n);
	for(int i = n - 1, op = 0, now = 0; i >= 0; --i) {
		if(S[i] == '1') {
			op ^= 1;
			if(i < k) {
				for(int j = 0; j < k; ++j) {
					unsigned long long tmp = 0;
					for(int l = 0, o = 1; l <= j; ++l, o = 1ll * o * now % mod) {
						tmp += 1ull * dp[i][op][j - l] * C[j][l] % mod * o;
						if(!(l & 15)) tmp %= mod;
					}
					tmp %= mod;
					fmod(ans += 1ll * tmp * a[j] % mod);
				}
			}
			fmod(now += pw2[i]);
		}
	}
	int sum = 0;
	for(int i = k; i < n; ++i)
		if(S[i] == '1') fmod(sum += pw2[i]);
	fmod(ans += 1ll * ((mod + 1) >> 1) * lagrange(sum, k, a) % mod);
	cout << ans << '\n';
}

T2 积木小赛

签到。

\(s\) 建子序列自动机,对 \(t\) 字符串哈希。

暴力枚举 \(t\)\(O(n^2)\) 个子串。

\([i,j]\) 这段区间可以通过子序列自动机找到最优的匹配位置转移到 \([i,j+1]\)。(显然越靠左越优吧),如果失配就退出。

复杂度 \(O(n^2\log n)\)

考场上脑抽了拿 map 去重,然后被卡常了/tuu

改单哈希,模数开成 1e16 级别,存下所有值然后 sort unique。

然后还是被卡常了/tuu

md 我松式基排你再卡?

然后过去了,\(1s+\to 0.3s\)

以前从来没感觉 sort 跑不动 1e7 啊,怎么回事?

哦,没开O2啊/tuu

Code
#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mkp(x,y) make_pair(x,y)
#define pb(x) push_back(x)
#define sz(v) (int)v.size()
typedef long long LL;
typedef double db;
template<class T>bool ckmax(T&x,T y){return x<y?x=y,1:0;}
template<class T>bool ckmin(T&x,T y){return x>y?x=y,1:0;}
#define rep(i,x,y) for(int i=x,i##end=y;i<=i##end;++i)
#define per(i,x,y) for(int i=x,i##end=y;i>=i##end;--i)
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
	while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
	return f?x:-x;
}
typedef unsigned long long ull;
const int N=3005;
int n,len,nxt[N][26],a[N],b[N],ans;
ull lsh[N*N/2];
char S[N],T[N];
const ull mod=10000000000000061ll;
const int base=131;
void qsort(ull*a,int n){
	static int cnt[256];
	static ull _b[N*N/2],*b=_b;
	for(int i=0;i<64;i+=8){
		memset(cnt, 0, sizeof(cnt));
		for(int j=n-1;j>=0;--j)++cnt[(a[j]>>i)&255];
		for(int j=1;j<256;++j)cnt[j]+=cnt[j-1];
		for(int j=n-1;j>=0;--j)b[--cnt[(a[j]>>i)&255]]=a[j];
		swap(a,b);
	}
}
signed main(){
	scanf("%d%s%s",&n,S+1,T+1);
	rep(i,1,n)a[i]=S[i]-'a',b[i]=T[i]-'a';
	rep(i,0,25)nxt[n][i]=n+1;
	per(i,n,1){
		rep(j,0,25)nxt[i-1][j]=nxt[i][j];
		nxt[i-1][a[i]]=i;
	}
	rep(i,1,n){
		int now=0;
		ull h=0;
		rep(j,i,n){
			now=nxt[now][b[j]];
			if(now>n)break;
			h=(h*base+b[j]+1)%mod;
			lsh[++len]=h;
		}
	}
	qsort(lsh+1,len);
	rep(i,1,len)if(lsh[i]!=lsh[i-1])++ans;
	cout<<ans<<'\n';
	return 0;
}

T3 岛屿探险

果然我的 DS 水平过逊了。

部分分启发性挺不错的,为良心出题人点赞!但是std为啥是cdq分治啊

考虑线段树分治,把每一个询问 \([l,r]\) 拆成 \(O(\log n)\) 段区间挂到线段树节点上,这样只用考虑一个集合的情况了。

然后也非常显然,把这个区间内的元素按照 \(b\) 升序排,这个节点的询问按照 \(d\) 降序排。

这么做是为了尝试去掉 \(\min\),否则根本没法做。

可以发现我们需要维护两个集合:

  • 第一个支持:
    • 加入一个数对 \((a,b)\)
    • 给定一个数 \(c\),查询有多少个数对满足 \(a\bigoplus c\le b\)
    • 删除一个数对。
  • 第二个支持:
    • 加入一个数 \(a\)
    • 给定两个数 \(c,d\),查询有多少个数满足 \(a\bigoplus c\le d\)

首先把区间内所有元素插到第一个集合,随着 \(d\) 减小会不断把第一个集合内的元素移到第二个集合。

可以发现这两个集合都可以用 \(Trie\) 树很方便的维护。

这个具体怎么维护还是建议自己想,当然代码里自己写的时候也加了点注释,可以参考。

复杂度 \(O(n\log n\log w)\)

然而我的常数还是非常大,以至于我都不能用 vector 存每个节点的询问,还得加上每个节点没有询问的减枝才能过去/tuu。

Code
#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mkp(x,y) make_pair(x,y)
#define pb(x) push_back(x)
#define sz(v) (int)v.size()
typedef long long LL;
typedef double db;
template<class T>bool ckmax(T&x,T y){return x<y?x=y,1:0;}
template<class T>bool ckmin(T&x,T y){return x>y?x=y,1:0;}
#define rep(i,x,y) for(int i=x,i##end=y;i<=i##end;++i)
#define per(i,x,y) for(int i=x,i##end=y;i>=i##end;--i)
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
	while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
	return f?x:-x;
}

const int N = 100005;
const int M = N << 2;
int n, m, ans[N];
struct node {
	int c, d, id;
	node() { c = d = id = 0; }
	node(int c_, int d_, int id_) { c = c_, d = d_, id = id_; }
} b[M], o[M];
int hed[N * 20], nxt[N * 20], et, to[N * 20];
inline void push(int p, int x) {
	to[++et] = x, nxt[et] = hed[p], hed[p] = et;
}
inline bool cmp1(const node &a, const node &b) {
	return a.d > b.d;
}
struct plc {
	int a, b;
	plc() { a = b = 0; }
	plc(int a_, int b_) { a = a_, b = b_; }
} a[N], w[N];
inline bool cmp2(const plc &a, const plc &b) {
	return a.b < b.b;
}

namespace tr1 {
/*
a_i xor c <= b_i
修改:在 a_i xor k <= b_i 的子树根打标记
查询:走一遍 c,统计走到的标记和
*/
const int T = N * 25 * 2;
int tot, tr[T][2], tag[T];
inline int New() {
	++tot;
	tr[tot][0] = tr[tot][1] = tag[tot] = 0;
	return tot;
}
inline void clear() { tot = 0, New(); }
inline void insert(const plc &x, int d) {
	int u = 1, v1 = x.a, v2 = x.b;
	for(int i = 23; i >= 0; --i) {
		int c = v1 >> i & 1;
		if(v2 >> i & 1) {
			if(!tr[u][c]) tr[u][c] = New();
			tag[tr[u][c]] += d;
			if(!tr[u][!c]) tr[u][!c] = New();
			u = tr[u][!c];
		} else {
			if(!tr[u][c]) tr[u][c] = New();
			u = tr[u][c];
		}
	}
	tag[u]+=d;
}
inline int query(const node &x) {
	int u = 1, res = 0, v = x.c;
	for(int i = 23; i >= 0; --i) {
		int c = v >> i & 1;
		u = tr[u][c], res += tag[u];
	}
	return res;
}

}

namespace tr2 {
/*
a_i xor c <= d
修改:统计每一个子树内 a 的个数 
查询:c 从上到下在 trie 上走,顺便统计答案,注意叶子结点贡献 
*/
const int T = N * 25;
int tot, sum[T], tr[T][2];
inline int New() {
	++tot;
	sum[tot] = tr[tot][0] = tr[tot][1] = 0;
	return tot;
}
inline void clear() { tot = 0, New();}
inline void insert(plc x, int d) {
	int u = 1, v = x.a;
	for(int i = 23; i >= 0; --i) {
		int c = v >> i & 1;
		if(!tr[u][c]) tr[u][c] = New();
		u = tr[u][c], sum[u] += d;
	}
}
inline int query(const node &x) {
	int u = 1, res = 0, v1 = x.c, v2 = x.d;
	for(int i = 23; i >= 0; --i) {
		int c = v1 >> i & 1;
		if(v2 >> i & 1) res += sum[tr[u][c]], u = tr[u][!c];
		else u = tr[u][c];
	}
	res += sum[u];
	return res;
}

}

#define lc (p << 1)
#define rc (p << 1 | 1)
void update(int ql, int qr, int d, int l, int r, int p) {
	if(ql <= l && r <= qr) return push(p, d), void();
	int mid = (l + r) >> 1;
	if(ql <= mid) update(ql, qr, d, l, mid, lc);
	if(mid < qr) update(ql, qr, d, mid + 1, r, rc);
}
/*
按照 b 递增 d 递减排
左半边 tr1 处理,右半边 tr2 处理
每处理询问前把 b > d 的移动到 tr2
*/
void solve(int l, int r, int p) {
	int t1 = 0, t2 = 0;
	rep(i, l, r) w[++t1] = a[i];
	for(int i = hed[p]; i; i = nxt[i])
		b[++t2] = o[to[i]];
	if(t2) {
		sort(b + 1, b + t2 + 1, cmp1);
		sort(w + 1, w + t1 + 1, cmp2);
		tr1::clear(),tr2::clear();
		rep(i, 1, t1) tr1::insert(w[i], 1);
		int it = t1;
		for(int i = 1; i <= t2; ++i) {
			while(it >= 1 && w[it].b > b[i].d)
				tr1::insert(w[it], -1), tr2::insert(w[it], 1), --it;
			ans[b[i].id] += tr1::query(b[i]) + tr2::query(b[i]);
		}
	}
	if(l == r) return;
	int mid = (l + r) >> 1;
	solve(l, mid, lc), solve(mid + 1, r, rc);
}
signed main() {
	n = read(), m = read();
	rep(i, 1, n) a[i].a = read(), a[i].b = read();
	rep(i, 1, m) {
		int l = read(), r = read(), c = read(), d = read();
		o[i] = node(c, d, i);
		update(l, r, i, 1, n, 1);
	}
	solve(1, n, 1);
	rep(i, 1, m) printf("%d\n", ans[i]);
	return 0;
}

总结

出题人太松了!!!

不开 O2 太恶心了!!!

题目质量不错,卡常毒瘤!!!

还有非常 /tuu 的是码量有点大啊,加起来码了 8.3k !!!

考场上自己傻逼没办法啊,T1 这么傻逼的错误调了一整场,,,/tuu

建议补题的时候不要开 O2,体验一下卡常的快感(逃

posted @ 2021-03-31 08:19  zzctommy  阅读(165)  评论(0编辑  收藏  举报