消失的运算符
题目描述
给定一个长度为 \(n\) 的合法表达式,并且只出现括号,减号和在 \([1,9]\) 之间的数字。其中减号代表运算符的空位,设有 \(m\) 个。
给定 \(k\),你要把 \(k\) 个减号替换成加号,把 \(m-k\) 个减号替换成乘号。求所有本质不同的替换方案表达式的和对 \(10^9+7\) 取模后的值。
\(1\le n\le 10^5,0\le m\le 2500\)。
解法
\(\text{Subtask 1}\):没有括号
因为要写 \(\mathcal O(m^2)\) 的算法,然后加了前缀和一直调调调,后来发现先开始的 \(\mathtt{dp}\) 方程有问题… 都是泪啊。
\(\mathtt{dp}\) 转移也很简单,首先找到减号的位置序列 \(p\),令 \(dp_{i,j}\) 为 \(1\) 到 \(p_i-1\) 之间的字符组成的所有表达式中,有 \(j\) 个加号的权值和。除此之外需要定义辅助数组 \(f_{i,j},g_{i,j}\),它们的状态和前文类似,但具体分别代表方案数与一坨奇怪的东西(后面再讲。
那么就有转移:
由于 \(mul(k+1,i)\) 这一项很难搞,我们考虑将其拆分成两个部分:令 \(pre_i\) 为前缀积,\(inv_i\) 是 \(pre_i\) 的逆元。转移就可以变成:
令
那么就有:
就做完了。代码太长了,就放链接吧。
\(\text{Subtask 2}\):有括号
用这个序列构造一棵括号树,相同深度的点所代表的区间就转化成上个子任务的 "数字"。
但是求几个区间的 \(mul()\) 似乎比较困难,我们不妨换一个 \(\mathtt{dp}\) 定义:令 \(f_{i,j}\) 为括号树上的点 \(i\)(它对应原序列的一段区间),有 \(j\) 个加号的权值和,\(g_{i,j}\) 为括号树上的点 \(i\),有 \(j\) 个加号的最后一段乘积和,\(cnt_{i,j}\) 就是方案数。
具体转移的时候,假设需要算出 \(i\) 的 \(\mathtt{dp}\) 值,我们先不将 \(f_{i,j}\) 加上最后一段乘积和。算完再加回去。
将 \(i\) 统治的区间划分成几个上个子任务的 "数字",先分别递归,返回时合并即可,这类似于 树形背包,复杂度是 \(\mathcal O(n^2)\) 的。实现时注意数字 \([l,r]\) 对应减号 \([l,r)\),在算大小的时候也要考虑。
代码
数据恢复
题目描述
给定一棵 \(n\) 个点的树,对于所有 \(2\le i\le n\),它的父亲为 \(f_i\),每个点有系数 \(a_i,b_i\)。
你需要求出一个的排列,满足对于 \(2\le i\le n\),\(f_i\) 都在 \(i\) 出现前出现。
这个排列的代价为:
求最大代价。
\(n\le 3\times 10^5\)。
解法
菊花图是没有 \(f_i\) 限制的情况,这提醒我们先思考没有限制怎么做。考虑相邻两个数 \(i,j(i<j)\),当它们的位置交换时 \(\delta=b_ja_i-b_ia_j\)。假设交换前更优,就可以得出 \(\frac{a_i}{b_i}<\frac{a_j}{b_j}\),令 \(v_i=\frac{a_i}{b_i}\),直接升序排然后选即可。
加上限制之后,还是升序排序,可以发现选 \(f_u\) 之后直接选 \(u\) 肯定是最优的。而且,由乘法分配律,我们惊喜地发现这两个点的信息是可以合并的!用一个堆维护升序,就是 \(\mathcal O(n\log n)\) 的。
另一道类似的题是 牛半仙的魔塔。
由于奇特的数据范围,你会发现牛半仙每次减少的血量都是正数,所以我们只需要最小化减少的血量即可(因为不会回血,所以末状态一定是血量最低时刻)。
最小化减少的血量似乎挺复杂的,其实还有一个性质:每个怪物存活的轮数是固定的。从而每个怪物对牛半仙的伤害是固定的(如果防御不增加的话)。所以最大化防御值减伤的效果即可。假设怪物 \(i\) 的存活轮数是 \(r_i\),增加防御为 \(w_i\)。还是考虑相邻两个数 \(i,j(i<j)\),当它们的位置交换时 \(\delta=w_jr_i-w_ir_j\)(减伤的效果)。假设交换前更优,就可以得出 \(\frac{w_i}{r_i}>\frac{w_j}{r_j}\),令 \(v_i=\frac{w_i}{r_i}\),这就变成了上面的问题。实际实现 还要模拟一下打怪。
代码
#include <cstdio>
#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((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-'),write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
#include <queue>
using namespace std;
typedef long long ll;
const int maxn = 3e5+5;
int n,fa[maxn],f[maxn];
int a[maxn],b[maxn],siz[maxn];
struct node {
double v;
int id,sz;
node() {}
node(double V,int ID,int SZ) {
v=V,id=ID,sz=SZ;
}
bool operator < (const node &t) const {
return v>t.v;
}
};
priority_queue <node> q;
void init() {
for(int i=1;i<=n;++i)
f[i]=i,siz[i]=1;
}
int Find(int x) {
return x==f[x]?x:f[x]=Find(f[x]);
}
int main() {
n=read(9); init();
for(int i=2;i<=n;++i)
fa[i]=read(9);
for(int i=1;i<=n;++i) {
a[i]=read(9),
b[i]=read(9);
if(i^1)
q.push(node(1.0*a[i]/b[i],i,1));
}
int x,y; ll ans=0;
while(!q.empty()) {
node t=q.top(); q.pop();
if(t.sz^siz[t.id])
continue;
x=Find(fa[t.id]);
y=Find(t.id);
if(x==y) continue;
ans += 1ll*b[x]*a[y];
b[x]+=b[y],a[x]+=a[y];
siz[f[y]=x]+=siz[y];
if(x^1)
q.push(node(1.0*a[x]/b[x],x,siz[x]));
}
print(ans,'\n');
return 0;
}
下落的小球
题目描述
有一棵 \(n\) 个点的树,根为 \(1\),每个点都有一个初始为 \(1\) 的标记值 \(s_i\)。对点 \(x\) 进行操作表示把 \(x\) 的祖先中深度最小并且 \(s_i=1\) 的 \(i\) 置为 \(0\)。
每个 叶子 有一个操作次数 \(a_i\),问有多少种不同的操作序列使得最后标记值全为 \(0\)。
\(n\leq 10^6\),\(\sum a_i=n\),保证若 \(i\) 非叶子则 \(a_i=0\)。
解法
记 \(b_i\) 为 \(i\) 子树的 \(a_i\) 之和,\(s_i\) 为子树大小。令 \(r_i=b_i-s_i\),如果任意 \(i\) 均满足 \(r_i\ge 0\),这个 \(a\) 序列就是可以实现的。具体就是若 \(r_i<0\),那么这棵子树不会被消耗完;反之,由于题目中保证了 \(\sum a_i=n\),所以不会出现 "供不应求" 的情况。
对于点 \(i\),考虑何时它的标记值为 \(0\)。走 \(r_i\) 步后,子树中每个点的标记值均为 \(1\),且此时子树操作次数也变成 \(s_i\),显然不再需要外部供给了。所以 \(r_i+1\) 步时 \(i\) 的标记值就是 \(0\) 了。将 \(r_i\) 步之内称为 \(S\) 部,剩余步称为 \(T\) 部。
套路地,我们从子树到根合并某节点儿子 \(x,y\) 的操作序列,令点 \(i\) 的操作序列方案数为 \(dp_i\)。由于共用父亲,当 \(x\) 失去外部供给时 \(y\) 还有外部供给显然是不合理的,所以 \(x,y\) 的 \(S,T\) 部一定是 \(S_x,S_y\) 自由组合,\(T_x,T_y\) 自由组合再将 \(S',T'\) 按 顺序 拼接。这就是个组合数嘛!
具体可以这样转移:
for(v \in Son_u) {
s[u]+=s[v],r[u]+=r[v];
dp[u] = dp[u]*dp[v]/(s[v]!)/(r[v]!);
}
dp[u] = dp[u]*(s[u]-1)!*(r[u]+1)!;
dp[u]*dp[v]
是选出 各自 排列顺序,剩下一坨就是组合数,化简发现中间一段可以抵消。由于 \(u\) 不是叶子,所以 \(\sum r_v=r_u+1\)。
古老的序列问题
题目描述
给定长度为 \(n\) 的序列 \(s\)。你需要回答 \(m\) 个询问,每次询问给出 \(L,R\),你需要计算以下式子:
其中 \(cost(l,r)\) 是 \([l,r]\) 中最大值和最小值的乘积。
\(1\le n,m\le 10^5,1\le s_i\le 10^9\)。
解法
\(\text{Subtask 1}\):\(s_i\le s_{i+1}\)
问题相当于求 \([L,R]\) 中两两数字的乘积。可以预处理出 \(suf_i=s_i\cdot \sum_{j=i}^n s_i\)。那么
另外中间还有几档部分分,但是我不会~
\(\text{Subtask 2}\):\(\mathcal O(n\log^2 n)\)
感觉这种离线询问题很多都可以用各种分治搞一个最优/次优解出来,以后不妨往上面靠靠……当然这道题就是打死我也想不到该怎么搞。
对于此题,我们考虑一个类似线段树的分治:对于每个节点 \(o\),统计它统治的区间 \([l,r]\) 的答案为 \(f_o\),这样当一个询问划分到节点 \(o\) 且完全包含节点 \(o\) 时就可以直接返回;反之继续递归。由于线段树区间查询为 \(\mathcal O(\log n)\),所以每个区间会被传递 \(\log n\) 次。更具体一点,自己处理经过 \(\text{mid}\) 的 \(cost(l',r')\),那么剩下的部分分给儿子处理即可。
从 \(\rm mid\) 开始向左移左指针,按这个顺序对右区间贡献(用数据结构维护)。这样我们将询问按 \(L\) 从大到小排序,就可以当左指针到 \(L_i\) 时,更新询问 \(i\)。我们把经过 \(\rm mid\) 的区间划分成四种情况:
- \(\min,\max\) 都在左区间。
- \(\max\) 在左区间。
- \(\min\) 在左区间。
- \(\min,\max\) 都在右区间。
由于左指针左移时,左区间的 \(\min,\max\) 一定趋向更优,所以可以用两个指针维护右区间满足 "\(\min,\max\) 都在左区间" 的右端点的边界 \(\text{MX},\text{MN}\),这是递增的。然后就可以得到四种情况的区间。
讲了这么多,究竟该如何维护 \(cost()\) 呢?建出四棵线段树分别代表四种情况(维护右区间),比如,情况二初始时就是给 \((\text{mid},r]\) 的 \(v_j\) 赋值为 \(\min_{i=\text{mid}+1}^j s_j\),在移动左指针时,找到情况二对应的区间,然后传递当前左区间的 \(\max\),区间内每个点的答案就需要累加 \(v_j\cdot \max\)。
感觉每一步都好妙啊。
\(\text{Subtask 3}\):\(\mathcal O(n\log n)\)
事实上,这种 "给定区间求所有子序列信息" 的题目有一个通用的思路:求历史的和。
比如此题,我们右移右端点到 \(i\),此时左端点 \([1,i]\) 表示 \(cost(\{1,2,...,i\},i)\)。当 \(i+1\) 时,\(cost(\{1,2,...,i\},i)\) 就变成了历史值。对于询问 \((l,r)\),我们就需要求出右端点为 \(r\) 时,区间 \([l,r]\) 的历史值。
可以用单调栈维护某个区间的 \(\min,\max\)(下文以 \(\min\) 为例)。在右端点移动时,左端点对应的 \(\min\) 可能会变化,如何修改?由于我们已经知道需要修改 \(\min\) 的区间,所以直接乘上原本 \(\min\) 的逆元即可。
具体可以用矩阵维护,令初始矩阵为 \(\begin{bmatrix}v&h \end{bmatrix}\)。第一想法是将转移矩阵写成这样:\(\begin{bmatrix} c&\text{len}\\ 0&1\end{bmatrix}\)。然而修改时我们并不知道区间的 \(\rm len\),所以将其设在初始矩阵的 \(v\) 处即可。需要注意的是,修改后没有将历史和更新,所以需要再乘一个矩阵更新历史和。
这就是 \(\mathcal O(n\log n)\) 的,实测比上面那个做法快了一倍有余,只有 \(600\) 多毫秒。
代码
\(\mathcal O(n\log^2 n)\):
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5+5;
const int mod = 1e9+7;
inline int inc(int x,int y) {
return x+y>=mod?x+y-mod:x+y;
}
int n,m,s[maxn],f[maxn<<2];
int ans[maxn],L,R;
int a_[maxn],b_[maxn],c_[maxn],d_[maxn];
struct node {
int l,r,id;
node() {}
node(int L,int R,int ID):l(L),r(R),id(ID) {}
bool operator < (const node &t) const {
return l>t.l;
}
};
vector <node> q[maxn<<2];
class SegmentTree {
public:
int la[maxn<<2],v[maxn<<2],sm[maxn<<2];
void build(int o,int l,int r,int *t) {
la[o]=sm[o]=0;
if(l==r) return v[o]=t[l],void();
int mid=(l+r)>>1;
build(o<<1,l,mid,t);
build(o<<1|1,mid+1,r,t);
v[o]=inc(v[o<<1],v[o<<1|1]);
}
void update(int o,int k) {
sm[o] = inc(sm[o],1ll*v[o]*k%mod);
la[o] = inc(la[o],k);
}
void pushDown(int o) {
if(!la[o]) return;
update(o<<1,la[o]);
update(o<<1|1,la[o]);
la[o]=0;
}
void modify(int o,int ql,int qr,int k,int l=L,int r=R) {
if(l>=ql and r<=qr)
return update(o,k),void();
int mid=(l+r)>>1;
pushDown(o);
if(ql<=mid) modify(o<<1,ql,qr,k,l,mid);
if(qr>mid) modify(o<<1|1,ql,qr,k,mid+1,r);
sm[o] = inc(sm[o<<1],sm[o<<1|1]);
}
int ask(int o,int lim,int l=L,int r=R) {
if(r<=lim) return sm[o];
int mid=(l+r)>>1,ret;
pushDown(o);
ret=ask(o<<1,lim,l,mid);
if(mid<lim)
ret=inc(ret,ask(o<<1|1,lim,mid+1,r));
return ret;
}
} A,B,C,D;
void dicon(int o,int l,int r) {
if(l==r) {
f[o] = 1ll*s[l]*s[l]%mod;
for(auto i:q[o])
ans[i.id] = inc(ans[i.id],f[o]);
return;
}
int mid=(l+r)>>1;
vector <node> cur;
for(auto i:q[o]) {
if(i.l<=mid and i.r>mid and !(i.l==l and i.r==r))
cur.push_back(i);
if(i.r<=mid) q[o<<1].push_back(i);
else if(i.l>mid) q[o<<1|1].push_back(i);
else if(!(i.l==l and i.r==r)) {
q[o<<1].push_back(node(i.l,mid,i.id));
q[o<<1|1].push_back(node(mid+1,i.r,i.id));
}
}
sort(cur.begin(),cur.end());
int mn=mod,mx=0;
for(int i=mid+1;i<=r;++i) {
mn=min(mn,s[i]);
mx=max(mx,s[i]);
a_[i]=1; // all in left
b_[i]=mn; // max in left
c_[i]=mx; // min in left
d_[i]=1ll*mx*mn%mod;
// all in right
}
L=mid+1,R=r;
A.build(1,mid+1,r,a_); B.build(1,mid+1,r,b_);
C.build(1,mid+1,r,c_); D.build(1,mid+1,r,d_);
int p=0,siz=cur.size(),MN=mid,MX=mid;
mn=mod,mx=0;
for(int i=mid;i>=l;--i) {
mn=min(mn,s[i]);
mx=max(mx,s[i]);
while(MN<r and s[MN+1]>=mn) ++MN;
while(MX<r and s[MX+1]<=mx) ++MX;
if(mid+1<=MN and mid+1<=MX)
A.modify(1,mid+1,min(MN,MX),1ll*mn*mx%mod);
if(MN<MX) B.modify(1,MN+1,MX,mx);
if(MN>MX) C.modify(1,MX+1,MN,mn);
if(MX+1<=r and MN+1<=r)
D.modify(1,max(MN,MX)+1,r,1);
while(p<siz and cur[p].l==i) {
ans[cur[p].id] = inc(
ans[cur[p].id],
inc(
inc(A.ask(1,cur[p].r),B.ask(1,cur[p].r)),
inc(C.ask(1,cur[p].r),D.ask(1,cur[p].r))
)
);
++p;
}
}
f[o] = inc(inc(A.ask(1,r),B.ask(1,r)),
inc(C.ask(1,r),D.ask(1,r)));
dicon(o<<1,l,mid),dicon(o<<1|1,mid+1,r);
f[o] = inc(f[o],inc(f[o<<1],f[o<<1|1]));
for(auto i:q[o])
if(i.l==l and i.r==r)
ans[i.id] = inc(ans[i.id],f[o]);
}
int main() {
n=read(9),m=read(9);
for(int i=1;i<=n;++i)
s[i]=read(9);
int x,y;
for(int i=1;i<=m;++i)
x=read(9),y=read(9),
q[1].push_back(node(x,y,i));
dicon(1,1,n);
for(int i=1;i<=m;++i)
print(ans[i],'\n');
return 0;
}
\(\mathcal O(n\log n)\):
#pragma GCC optimize(2)
#include <cstdio>
#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((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(const T x) {
if(x<0) {
putchar('-');
write(-x);
return;
}
if(x>9) write(x/10);
putchar(x%10^48);
}
#include <vector>
using namespace std;
const int maxn = 1e5+5;
const int mod = 1e9+7;
inline int inc(int x,int y) {
return x+y>=mod?x+y-mod:x+y;
}
int n,m,s[maxn],ans[maxn];
int mn[maxn],mn_l,mx[maxn],mx_l;
struct Q {
int l,id;
Q() {}
Q(int L,int ID):l(L),id(ID) {}
};
vector <Q> q[maxn];
struct mat {
int a[2][2];
mat() {
a[0][0]=a[0][1]=a[1][0]=a[1][1]=0;
}
mat operator * (const mat &t) const {
mat r;
for(int i=0;i<2;++i)
for(int j=0;j<2;++j)
if(a[i][j])
for(int k=0;k<2;++k)
r.a[i][k]=inc(
r.a[i][k],
1ll*a[i][j]*t.a[j][k]%mod
);
return r;
}
bool operator == (const mat &t) const {
for(int i=0;i<2;++i)
for(int j=0;j<2;++j)
if(a[i][j]^t.a[i][j])
return 0;
return 1;
}
void Print() {
puts("----------");
for(int i=0;i<2;++i) {
for(int j=0;j<2;++j)
print(a[i][j],' ');
puts("");
}
}
} e,upd,k,t[maxn<<2],la[maxn<<2];
int inv(int x,int y=mod-2) {
int r=1;
while(y) {
if(y&1) r=1ll*r*x%mod;
x=1ll*x*x%mod; y>>=1;
}
return r;
}
void initMat() {
e.a[0][0]=e.a[1][1]=1;
upd.a[0][0]=upd.a[1][1]=upd.a[0][1]=1;
}
void getMat(int x) {
k.a[0][0]=x,k.a[1][1]=1;
k.a[0][1]=k.a[1][0]=0;
}
void build(int o,int l,int r) {
t[o].a[0][0]=r-l+1;
la[o]=e; // important!
if(l==r) return;
int mid=l+r>>1;
build(o<<1,l,mid);
build(o<<1|1,mid+1,r);
}
void pushDown(int o) {
if(la[o]==e) return;
t[o<<1]=t[o<<1]*la[o];
t[o<<1|1]=t[o<<1|1]*la[o];
la[o<<1]=la[o<<1]*la[o];
la[o<<1|1]=la[o<<1|1]*la[o];
la[o]=e;
}
void modify(int o,int l,int r,int L,int R) {
if(l>=L and r<=R) {
t[o]=t[o]*k; la[o]=la[o]*k;
return;
}
int mid=l+r>>1;
pushDown(o);
if(L<=mid) modify(o<<1,l,mid,L,R);
if(R>mid) modify(o<<1|1,mid+1,r,L,R);
t[o].a[0][0]=inc(t[o<<1].a[0][0],t[o<<1|1].a[0][0]);
t[o].a[0][1]=inc(t[o<<1].a[0][1],t[o<<1|1].a[0][1]);
}
int ask(int o,int l,int r,int L,int R) {
if(l>=L and r<=R) return t[o].a[0][1];
int mid=l+r>>1,ret=0;
pushDown(o);
if(L<=mid) ret=ask(o<<1,l,mid,L,R);
if(R>mid) ret=inc(ret,ask(o<<1|1,mid+1,r,L,R));
return ret;
}
int main() {
n=read(9),m=read(9);
for(int i=1;i<=n;++i)
s[i]=read(9);
int x,y;
for(int i=1;i<=m;++i)
x=read(9),y=read(9),
q[y].push_back(Q(x,i));
initMat(); build(1,1,n);
for(int i=1;i<=n;++i) {
while(mn_l and s[mn[mn_l]]>=s[i]) {
getMat(inv(s[mn[mn_l--]]));
modify(1,1,n,mn[mn_l]+1,mn[mn_l+1]);
}
while(mx_l and s[mx[mx_l]]<=s[i]) {
getMat(inv(s[mx[mx_l--]]));
modify(1,1,n,mx[mx_l]+1,mx[mx_l+1]);
}
getMat(s[i]);
modify(1,1,n,mx[mx_l]+1,i);
modify(1,1,n,mn[mn_l]+1,i);
k=upd;
modify(1,1,n,1,i);
mx[++mx_l]=mn[++mn_l]=i;
for(int j=0;j<q[i].size();++j)
ans[q[i][j].id]=ask(1,1,n,q[i][j].l,i);
}
for(int i=1;i<=m;++i) print(ans[i],'\n');
return 0;
}
后记
感觉这类 "区间的区间" 问题已经很套路了……
比如还有 这道题,同样可以用猫树或矩阵求解。不过还有一个更快,而且更好写(不知道比猫树好写多少倍)的做法 —— 还是向右移右端点,维护左端点的线段树。在线段树上新维护两个值 \(h,\rm tag\),分别表示区间 \([l,r]\) 的所有子区间值为零的个数与当前区间值为零的个数需要累加进答案多少次。建立 \(\text{tag}\) 的缘由主要是有些左端点在右移右端点时,区间(比如左端点 \(i\),右端点 \(j\),此时对应区间 \([i,j]\),右移之后变成 \([i,j+1]\))的值是否为零的状态 没有改变。
具体来说,每次更新线段树后在根节点处的 \(\text{tag}\) 加一,pushDown()
时必须要满足根与儿子的 \(\min\) 相等(一定是零)。
可以看看 这道题。