\(\cal T_1\) 草莓蛋糕 / cake
Description
题目背景
题目描述
在一起将 \(\rm a\) 城的蛋糕店都吃过一遍后,你和唐芯都各自选出了自己最心仪的蛋糕店 —— "兰璱辰" 店与 "澄璱辰" 店,下简记为 \(\rm L\) 店与 \(\rm C\) 店。作为甜品的狂热爱好者,唐芯每周都会去 \(\rm C\) 店买她心爱的草莓蛋糕来吃,不过,"两个总比一个好",所以她总是帮你去 \(\rm L\) 店也买一份,再 "顺便帮你吃一点儿"。
对于每个草莓蛋糕都有两个值 \(c,y\),分别代表蛋糕的卡路里和美味度,奇怪的是,美味度越小代表蛋糕越美味。同时,两家蛋糕店里卖的草莓蛋糕可以分别被表示成 多重集 \(L,C\).
蛋糕店里卖的草莓蛋糕不会是一成不变的。具体地,在唐芯相邻两次买蛋糕的时间间隔中,\(\rm L\) 店与 \(\rm C\) 店中会有一家店进行一个草莓蛋糕的上新或下架。另外需要强调的是,唐芯购买蛋糕并不会造成多重集 \(L,C\) 的变化。
由于唐芯会 "吃一点儿" 你的那份,所以对于一组买蛋糕方案 \(a\in L,b\in C\),唐芯会增加的卡路里是 \(a_c+b_c\),感受到的美味度是 \(a_y+b_y\). 作为一名女演员,唐芯必须控制她的体重,但她同时也不想在享受美食的过程中太委屈自己。也就是说,定义 \(\max\{a_c+b_c,a_y+b_y\}\) 为唐芯对一组买蛋糕方案的不喜爱度,她希望能找到一组买蛋糕方案,使得其不喜爱度最小。
由于唐芯马上要赶去《鲛人泪》剧组进行拍摄,所以她将接下来 \(Q\) 周的买蛋糕任务交给了你。为了方便起见,你只需要告诉她每周的最小不喜爱度即可。
\(Q\leqslant 10^6,1\leqslant c,y\leqslant 10^9\).
Solution
一些闲话 & 免责声明:
我真的 没想过卡常,真的 没想过卡常,真的 没想过卡常 இ௰இ!
先开始设置成 \(\text{3 s}\) 确实不太合理(虽然也不是我设的(进行推锅,后来改成 \(\text{4 s}\),想着赛后重测应该都可以 A(真的是标程的两倍啊),没有想到还是有人没有过 qwq(不过同队的一位不愿透露姓名的同学的 \(\text{1 log}\) 做法没有跑过 \(\text{2 log}\) 我也只能哈哈哈哈哈哈哈哈哈了)。总之被机房众 D 了,
因为某种比较奇怪的原因这个 "被 D" 的 debuff 转移到另一位出题人身上了,感觉挺抱歉的。总之,这里对因为被卡常没有随切这道题的同学说句对不起!我谢罪(滑跪)。
\(Q\leqslant 600\)
暴力维护 \(L,C\),每次询问进行两家店蛋糕的两两匹配取 \(\min\),复杂度 \(\mathcal O(Q^3)\).
\(Q\leqslant 5000\)
用两个 \(\rm multiset\) 维护 \(L,C\),再开一个 \(\rm multiset\) 维护 \(\max\{a_c+b_c,a_y+b_y\}\),当上新/下架一个草莓蛋糕时,将这个蛋糕的信息与另一家店的所有蛋糕进行匹配,修改维护 \(\max\) 的 \(\rm multiset\) 的信息即可。复杂度 \(\mathcal O(Q^2\log Q)\).
\(Q\leqslant 10^6\)
题目要求 \(\min_{a\in L,b\in C}\max\{a_c+b_c,a_y+b_y\}\),这个 \(\max\) 很烦人,把两个蛋糕绑在一起,所以考虑分类讨论去除 \(\max\):若 \(a_c+b_c\geqslant a_y+b_y\),也就是 \(a_c-a_y\geqslant b_y-b_c\),对每个蛋糕记一个判据量 \(h\),对于 \(a\in L\),\(h=a_c-a_y\);对于 \(b\in C\),\(h=b_y-b_c\). 那么对于 \(a\in L\),有集合 \(B=\{b_h\leqslant a_h,b\in C\}\),\(a\) 与集合 \(B\) 中任意元素的最大值都是 \(a_c+b_c\),显然选择集合 \(B\) 中 \(b_c\) 最小值是最优的。同时,如果从固定 \(b\in C\) 的角度来看,我们要选择对应集合 \(A\) 中 \(a_c\) 的最小值。至此,我们成功地去除了 \(\max\).
考虑用线段树维护这个过程。发现选哪边作为最大值就是比较判据量的大小关系,所以开一个值域为 \(h\) 值域的线段树,每个节点维护判据量在此区间的两家蛋糕的信息。只需要分别维护两家蛋糕的 \(\min c,\min y\) 以及最终答案即可。加入与删除可以通过维护叶子节点的 \(\rm multiset\) 实现。复杂度 \(\mathcal O(Q\log Q)\).
这里更新一个彩蛋:赛时看到 \(\sf{Cirno\_9}\) 的 \(\color{red}{\text{wa 95}'}\) 的时候非常慌张,害怕把标程写挂了。之后心惊胆战地 debug 了一会后发现他没有判等(等下为啥这个没写只 wa 了一组啊。
Code
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp]=x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
# include <set>
# include <iostream>
# include <algorithm>
using namespace std;
const int maxn = 1e6+5;
const int infty = 2e9+9;
int n,val[maxn];
struct Q { int opt,d,c,y; } s[maxn];
struct node {
int v[2][2], ans;
void pushUp() {
if(v[0][0]!=infty && v[1][0]!=infty)
ans = v[0][0]+v[1][0];
else ans = infty;
}
node operator + (const node& t) const {
node r;
for(int i=0;i<2;++i) for(int j=0;j<2;++j)
r.v[i][j] = min(v[i][j],t.v[i][j]);
r.ans = min(ans,t.ans);
if(v[0][1]==infty || t.v[1][1]==infty);
else r.ans = min(r.ans,v[0][1]+t.v[1][1]);
if(t.v[0][0]==infty || v[1][0]==infty);
else r.ans = min(r.ans,t.v[0][0]+v[1][0]);
return r;
}
} t[maxn<<2];
multiset <int> st[maxn][2][2];
void ins(int o,int l,int r,int p,int c,int y,bool d) {
if(l==r) {
st[l][d][0].insert(c),
st[l][d][1].insert(y);
t[o].v[d][0] = min(t[o].v[d][0],c),
t[o].v[d][1] = min(t[o].v[d][1],y);
t[o].pushUp(); return;
} int mid = l+r>>1;
if(p<=mid) ins(o<<1,l,mid,p,c,y,d);
else ins(o<<1|1,mid+1,r,p,c,y,d);
t[o] = t[o<<1]+t[o<<1|1];
}
void del(int o,int l,int r,int p,int c,int y,bool d) {
if(l==r) {
st[l][d][0].erase(st[l][d][0].lower_bound(c)),
st[l][d][1].erase(st[l][d][1].lower_bound(y));
if(st[l][d][0].empty()) t[o].v[d][0] = infty;
else t[o].v[d][0] = *st[l][d][0].begin();
if(st[l][d][1].empty()) t[o].v[d][1] = infty;
else t[o].v[d][1] = *st[l][d][1].begin();
t[o].pushUp(); return;
} int mid = l+r>>1;
if(p<=mid) del(o<<1,l,mid,p,c,y,d);
else del(o<<1|1,mid+1,r,p,c,y,d);
t[o] = t[o<<1]+t[o<<1|1];
}
void build(int o,int l,int r) {
for(int i=0;i<2;++i) for(int j=0;j<2;++j)
t[o].v[i][j] = infty; t[o].ans = infty;
if(l==r) return; int mid = l+r>>1;
build(o<<1,l,mid), build(o<<1|1,mid+1,r);
}
int main() {
freopen("cake.in","r",stdin);
freopen("cake.out","w",stdout);
n = read(9);
for(int i=1;i<=n;++i) {
s[i].opt=read(9), s[i].d=read(9),
s[i].c=read(9), s[i].y=read(9);
val[i] = s[i].d? s[i].y-s[i].c: s[i].c-s[i].y;
}
sort(val+1,val+n+1);
const int rane = unique(val+1,val+n+1)-val-1;
build(1,1,rane);
for(int i=1;i<=n;++i) {
int x = s[i].d? s[i].y-s[i].c: s[i].c-s[i].y;
int h = lower_bound(val+1,val+rane+1,x)-val;
if(s[i].opt) ins(1,1,rane,h,s[i].c,s[i].y,s[i].d);
else del(1,1,rane,h,s[i].c,s[i].y,s[i].d);
print(t[1].ans==infty? -1: t[1].ans,'\n');
}
return 0;
}
\(\cal T_2\) 矩阵补全 / completion
Description
本来今天小 Q 是想让你补全一个矩阵的,但是他突然发现自己把矩阵剩下部分的信息也搞丢了。具体来讲,他的矩阵是一个 \(n\) 行 \(m\) 列的 01 矩阵 \(A\)(下标从 \(0\) 开始),有如下性质:
- 把每一行的值拼起来形成一个二进制数,这个数属于一个给定的集合 \(S\),即满足 \(\displaystyle ∀0 \leqslant i < n, S ∋ \sum^{m−1}_{j=0} a_{ij}2^j\);
- 对于每一列的性质,我们用两个长度为 \(m\) 的数组 \(b\) 和 \(c,c_i ∈ [0, 1]\) 来描述。对于每个 \(0 \leqslant i < m\):
- 若 \(b_i = 0\),则满足第 \(i\) 列的每个元素都等于 \(c_i\);
- 若 \(b_i = 1\),则满足第 \(i\) 列的所有元素的按位或和等于 \(c_i\);
- 若 \(b_i = 2\),则满足第 \(i\) 列的所有元素的按位与和等于 \(c_i\);
- 若 \(b_i = 3\),则满足第 \(i\) 列的所有元素的按位异或和等于 \(c_i\).
不幸的是,小 Q 把数组 \(c\) 也给搞忘了,只记得 \(S\) 和 \(b\),所以你需要帮助他回忆起数组 \(c\).
具体而言,他会向你询问 \(Q\) 次,每次给出一个可能的 \(c\),你需要回答满足条件的矩阵的个数。
\(n\leqslant 10^9,m\leqslant 20\).
Solution
不妨先考虑 \(b_i=1\) 的情况,那么把每一行的元素看成一个数字,实际上就是对一个全为 \(1\) 的数组 \(P\) 进行 \(n\) 次或卷积得到 \(P'\),每次询问 \(c\) 就是 \(P'_c\)。这个可以用 \(\tt FWT\) + 快速幂加速到 \(\mathcal O((m+\log n)\cdot 2^m)\).
先抛出正解的写法:在 \(\tt FWT\) 途中考虑每个 \(2\) 的幂,对应的 \(b\) 是什么就做什么 \(\tt FWT\)(\(b=0\) 时什么也不干)。为啥这是对的?考虑 \(\tt FWT\) 的过程实际上就是不断将二进制位拆分的迭代,所谓递推也是 模拟 此运算在这一位上如何运算,所以它本身就是可以被拆分的。
Code
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp] = x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
const int MAXN = 1<<20;
const int mod = 1e9+7, iv = (mod+1>>1);
int qkpow(int x,int y,int r=1) {
for(; y; y>>=1, x=1ll*x*x%mod)
if(y&1) r=1ll*r*x%mod; return r;
}
int adj(int x) { return (x+mod)%mod; }
char str[MAXN];
int n, m, lim, f[MAXN], b[30];
void fwt(int* f,int opt=1) {
for(int w=0, l=2; w<m; ++w, l<<=1) if(b[w]) {
if(b[w]==1) {
for(int o=0, p=l>>1; o<lim; o+=l)
for(int i=o; i<o+p; ++i)
f[i+p] = (f[i+p]+f[i]*opt)%mod;
} else if(b[w]==2) {
for(int o=0, p=l>>1; o<lim; o+=l)
for(int i=o; i<o+p; ++i)
f[i] = (f[i]+f[i+p]*opt)%mod;
} else {
for(int o=0, p=l>>1; o<lim; o+=l)
for(int i=o; i<o+p; ++i) {
const int x=f[i], y=f[i+p];
f[i] = (x+y)%mod, f[i+p] = (x-y)%mod;
if(opt==-1) f[i] = 1ll*f[i]*iv%mod,
f[i+p] = 1ll*f[i+p]*iv%mod;
}
}
}
}
int main() {
freopen("completion.in","r",stdin);
freopen("completion.out","w",stdout);
n=read(9), m=read(9), scanf("%s",str); lim=1<<m;
for(int i=0;i<lim;++i) f[i] = str[i]-'0';
for(int i=0;i<m;++i) b[i]=read(9);
fwt(f); for(int i=0;i<lim;++i) f[i]=qkpow(f[i],n);
fwt(f,-1); for(int i=0;i<lim;++i) f[i] = adj(f[i]);
for(int Q=read(9); Q; --Q) print(f[read(9)],'\n');
return 0;
}
\(\cal T_3\) 万灵药 / elixir
Description
给定只包含 \(\text{a,b,c,d}\) 的长度为 \(n\) 的字符串 \(S\),进行 \(Q\) 次操作 \((x,y)\):
- 在集合中插入前缀 \(x,y\) 的 最长公共后缀(注意 \(x,y\) 可以相同),若集合已经存在相同的 字符串(注意不是相同 \(x,y\)),则在集合中删除这个字符串;
- 定义一个集合的 亲和指数 为集合中 所有字符串两两最长公共前缀 的长度之和,请输出当前集合的亲和指数。
\(n,Q\leqslant 5\cdot 10^5\).
Solution
首先建出 \(\rm sam\),那么任意两个前缀的最长公共后缀就是它们在 \(\text{parent tree}\) 上 \(\rm lca\) 的最长串。由于 \(\rm sam\) 点数级别是 \(\mathcal O(n)\) 的,所以实际上集合大小级别也是 \(\mathcal O(n)\) 的。
考虑用 \(\rm trie\) 树维护这个集合,粗略来看插入复杂度是 \(\mathcal O(n^2)\) 的,然后就可以转化为求 \(\rm trie\) 树上两两关键节点的 \(\rm lca\) 的深度之和,这个问题有一个较为经典的解决方案 —— 插入点 \(u\) 时,查询 \(u\) 到根的权值和,就是此次操作亲和指数的增加量,然后将 \(u\) 到根路径上每个点都加一,删除同理,复杂度是两只 \(\log\) 的。或者也可以用全局平衡二叉树做到一只 \(\log\).
事实上,\(\rm trie\) 树可以做到 \(\mathcal O(n)\) 建树。考虑每个在 \(\text{parent tree}\) 上的节点 \(v\),一定存在一个节点 \(u\) 可以通过在后面加一个字符转移到 \(v\),且 \(u\) 内最长字符串为 \(v\) 去掉最后一个字符的字符串。\(u,v\) 的关系就和 \(\rm trie\) 树的父子关系非常类似了,这样建树即可做到 \(\mathcal O(n)\).
再提一嘴那个 \(\mathcal O(n^2)\) 的建树方法,实际上是对 \(\rm sam\) 上每个节点都维护 \(\text{R}\) 表示在原串上的右端点。叶子节点的 \(\rm R\) 是已知的,可以直接上传到 \(\text{parent tree}\) 上的父亲。事实上这个暴力比正解难写。
Code
虽然写的是树剖,但还是跑得挺快的,至少交上去和全局平衡二叉树差不多来着。
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp] = x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
# include <vector>
# include <cstring>
# include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 5e5+5;
const int MAXN = maxn<<1;
char str[maxn];
bool vis[MAXN];
int n, all, Ref[maxn];
namespace fwt {
ll c1[MAXN], c2[MAXN];
int lowbit(int x) { return x&-x; }
void add(int l,int r,int k) {
for(const int coe=(l-1)*k; l<=all; l+=lowbit(l))
c1[l] += k, c2[l] += coe; ++ r;
for(const int coe=(r-1)*(-k); r<=all; r+=lowbit(r))
c1[r] -= k, c2[r] += coe;
}
ll ask(int l,int r,ll ret=0) {
for(const int coe=r; r; r-=lowbit(r))
ret += c1[r]*coe-c2[r]; -- l;
for(const int coe=l; l; l-=lowbit(l))
ret -= c1[l]*coe-c2[l]; return ret;
}
}
struct HLD {
vector <int> e[MAXN];
int dep[MAXN], son[MAXN], sz[MAXN];
int Dfn, tp[MAXN], f[MAXN], dfn[MAXN];
void dfs1(int u) {
sz[u] = 1;
for(const auto& v:e[u]) {
dep[v] = dep[f[v]=u]+1;
dfs1(v), sz[u]+=sz[v];
if(sz[son[u]]<sz[v]) son[u]=v;
}
}
void dfs2(int u,int t) {
tp[u]=t, dfn[u]=++Dfn;
if(son[u]) dfs2(son[u],t);
for(const auto& v:e[u])
if(son[u]!=v) dfs2(v,v);
}
int lca(int x,int y) {
for(; tp[x]^tp[y]; x=f[tp[x]])
if(dep[tp[x]]<dep[tp[y]]) swap(x,y);
return dep[x]<dep[y]? x: y;
}
void modify(int x,int k) {
for(; tp[x]^1; x=f[tp[x]])
fwt::add(dfn[tp[x]],dfn[x],k);
fwt::add(2,dfn[x],k);
}
ll query(int x,ll ret=0) {
for(; tp[x]^1; x=f[tp[x]])
ret += fwt::ask(dfn[tp[x]],dfn[x]);
return ret+fwt::ask(2,dfn[x]);
}
} T, trie;
namespace sam {
int las=1, cnt=1;
struct node { int len,fa,to[4]; } t[MAXN];
void ins(int id) {
int c = str[id]-'a'; Ref[id] = cnt+1;
int cur=++cnt, p=las; t[cur].len=t[las].len+1;
for(; p && !t[p].to[c]; p=t[p].fa) t[p].to[c]=cur;
if(!p) t[cur].fa=1;
else {
int q = t[p].to[c];
if(t[q].len==t[p].len+1) t[cur].fa=q;
else {
int now = ++cnt; t[now]=t[q];
t[now].len = t[p].len+1;
t[cur].fa = t[q].fa = now;
for(; p && t[p].to[c]==q; p=t[p].fa) t[p].to[c]=now;
}
}
las=cur;
}
void consTree() {
for(int i=2;i<=cnt;++i)
T.e[t[i].fa].emplace_back(i);
for(int i=1;i<=cnt;++i) {
for(int j=0;j<4;++j) {
if(t[i].to[j] && t[t[i].to[j]].len==t[i].len+1)
trie.e[i].emplace_back(t[i].to[j]);
}
}
}
}
void CandyMyWife() {
static long long ans=0;
int x=read(9), y=read(9);
int z = T.lca(Ref[x],Ref[y]);
if(vis[z]) {
trie.modify(z,-1);
ans -= trie.query(z);
} else {
ans += trie.query(z);
trie.modify(z,1);
} vis[z] ^= 1;
print(ans,'\n');
}
int main() {
scanf("%s",str+1), n=strlen(str+1);
for(int i=1;i<=n;++i) sam::ins(i);
sam::consTree();
T.dfs1(1), T.dfs2(1,1), trie.dfs1(1), trie.dfs2(1,1);
all = T.Dfn;
for(int Q=read(9); Q; --Q) CandyMyWife();
return 0;
}