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\) 其实是由很多项式加起来得到的,推的时候应该展开再合并。
假设有值的位置有 \(q\) 个,分别为 \(x_1,x_2,\cdots,x_q\)。
那么 \(\operatorname{TRANS}\) 就是把
变成
可以 \(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,体验一下卡常的快感(逃