( ext{[SHOI 2016] })随机序列
解法
不妨先设 (dp_i) 为以 (i) 为结尾的乘积,前 (i) 个数组成表达式的和。就有转移:
其中 (pre_i) 是前缀积。单独 (pre_i) 表示新增 (1) 到 (i) 的乘积,剩余以 (i) 为结尾的乘积的左端点就属于 ((1,i))。本来我们需要计算新增的以 (i) 为结尾的乘积的贡献,但题目中可以添加加号和减号,所以新增贡献就刚好抵消了!所以 (sum) 前的系数 (2) 就是枚举加/减两种情况。
这个 (mathtt{dp}) 可以用前缀和优化成 (mathcal O(n)) 的,更进一步该如何优化呢?一般我们可以利用 相同的转移方式。这种时候你可以无脑上矩阵,或者,由于转移方式是不变的,我们可以计算 (pre_i) 对答案贡献的系数。这样我们就只用简单维护前缀积了。
这个系数也很好推,考虑 (pre_i) 对 (dp_i) 的贡献系数为 (1),对 (dp_{i+1}) 的贡献系数为 (2),对 (dp_{i+2}) 的贡献系数为 (6)… 代码中记这个系数为 (f_i)。总复杂度 (mathcal O(nlog n))。
但是这题还有数字为 (0) 的情况,它没有逆元。有一件很恼火的事情是,如果我们真的将 (0) 在维护前缀积的线段树上修改(假设它的位置为 (p)),在这之后,(p) 之后的位置的数字变化就无法被记录(因为它始终都为 (0)),再将 (p) 上的 (0) 改回来时,有些修改就被遗漏了。我们干脆开一个 set
记录 (0) 出现的位置,如果在初始数列出现就记为 (1),修改的时候不改 (0)。询问就找到第一个出现 (0) 的位置 (x),只询问 ([1,x]) 的答案即可。
看了份题解,感觉写得好简单…
其实根本不需要 (mathtt {dp}),直接列出答案的式子(其实就是我推了半天的 (f_i)):
还有一种处理 (0) 的方法:维护区间积。大概是这样:
Mul[i]=Mul[lc]*Mul[rc]%Mod;
Ans[i]=(Mul[lc]*Ans[rc]+Ans[lc])%Mod;
代码
#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 <set>
using namespace std;
const int maxn=1e5+5,mod=1e9+7;
int n,q,a[maxn],dp[maxn],f[maxn];
struct node {
int v,la,ans;
} t[maxn<<2];
set <int> s;
set <int> :: iterator it;
int inc(int x,int y) {
return x+y>=mod?x+y-mod:x+y;
}
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 pushUp(int o) {
t[o].ans=inc(t[o<<1].ans,t[o<<1|1].ans);
}
void build(int o,int l,int r) {
t[o].la=1;
if(l==r) {
t[o].v=a[l]?a[l]:1;
t[o].ans=1ll*dp[l]*f[l]%mod;
return;
}
int mid=l+r>>1;
build(o<<1,l,mid);
build(o<<1|1,mid+1,r);
pushUp(o);
}
void init() {
dp[0]=1;
for(int i=1;i<=n;++i)
if(a[i])
dp[i]=1ll*dp[i-1]*a[i]%mod;
else dp[i]=dp[i-1];
int tmp=0; f[n]=1;
for(int i=n-1;i>=1;--i) {
f[i]=inc(tmp,2ll*f[i+1]%mod);
tmp=inc(tmp,2ll*f[i+1]%mod);
}
build(1,1,n);
}
void pushDown(int o) {
if(t[o].la==1) return;
t[o<<1].ans=1ll*t[o<<1].ans*t[o].la%mod;
t[o<<1|1].ans=1ll*t[o<<1|1].ans*t[o].la%mod;
t[o<<1].la=1ll*t[o<<1].la*t[o].la%mod;
t[o<<1|1].la=1ll*t[o<<1|1].la*t[o].la%mod;
t[o].la=1;
}
void modify(int o,int l,int r,int L,int R,int k) {
if(l>R or r<L) return;
if(l>=L and r<=R) {
t[o].ans=1ll*t[o].ans*k%mod;
t[o].la=1ll*t[o].la*k%mod;
return;
}
int mid=l+r>>1;
pushDown(o);
modify(o<<1,l,mid,L,R,k);
modify(o<<1|1,mid+1,r,L,R,k);
pushUp(o);
}
void change(int o,int l,int r,int p,int k) {
if(l==r) return t[o].v=k,void();
int mid=l+r>>1;
if(p<=mid) change(o<<1,l,mid,p,k);
else change(o<<1|1,mid+1,r,p,k);
}
int ask_v(int o,int l,int r,int p) {
if(l==r) return t[o].v;
int mid=l+r>>1;
if(p<=mid) return ask_v(o<<1,l,mid,p);
return ask_v(o<<1|1,mid+1,r,p);
}
int query(int o,int l,int r,int L,int R) {
if(L>R or l>R or r<L) return 0;
if(l>=L and r<=R) return t[o].ans;
int mid=l+r>>1;
pushDown(o);
return inc(query(o<<1,l,mid,L,R),
query(o<<1|1,mid+1,r,L,R));
}
int main() {
n=read(9),q=read(9);
for(int i=1;i<=n;++i) {
a[i]=read(9);
if(!a[i])
s.insert(i);
}
init();
while(q--) {
int x,y;
x=read(9),y=read(9);
if(!y) s.insert(x);
else {
if(s.count(x))
s.erase(x);
}
if(y)
modify(1,1,n,x,n,1ll*y*Inv(ask_v(1,1,n,x))%mod);
if(!s.empty()) {
it=s.begin();
print(query(1,1,n,1,*it-1),'
');
}
else print(t[1].ans,'
');
if(y) change(1,1,n,x,y);
}
return 0;
}
严微的暗杀
题目描述
由于严微最近娶了老婆(许某),她经营的照相馆承担不起老婆和好运气的生活开销,再加上她们将迎来一个小宝宝,她不得不接受了更多的任务 —— 当然是运送尸体送快递!
上海有 (n) 个地点,(m) 条路。每条路有一定的长度,保证这些地点可以互相到达。严微此时先后接到了 (k) 个订单,第 (i) 个订单要求严微在 (a_i) 位置接货,并送达 (b_i) 位置。
但严微是一个职业杀手,怎么可能乖乖骑着她的小摩托送快递?她拥有一把传送枪,假设 (u,v) 都有传送门,那么她可以花费 (0) 时间从 (u) 到达 (v)。但是只能在经过某地时放置传送门,且同一时刻至多存在 两扇 传送门。
严微做事十分严谨懒,所以她一次只会取一个货物,且一定会按顺序送货。
由于她想早点回家和老婆酱酱酿酿,所以她想知道她送完货需要的最小时间。身为她们的 ( m cpg),你能不热血沸腾吗?你能不管吗?帮帮她们!
(1le n,kle 300,mle 40000,wle 10^9)。
注意:严微开始时在 (1) 号点。
解法
照搬题解。
首先可以 ( m Floyd) 处理最短路。我们可以将一次快递拆成按顺序经过两个点,具体可以处理出长度为 (2k+1) 的 (a) 数组表示必经点。
设 (dp_{i,j,x,y}) 为经过前 (i) 个必经点,现在在 (j),传送门是 (x,y) 的答案。不过,事实上我们并不需要存储两个传送门,因为当严微到达 (j) 且进行传送时,(x,y) 中必有一个为 (j),且无论怎样都可以立即在 (j) 新开一个传送门。
现在 (mathtt{dp}) 状态节省到了三维,但还不够。唯一看上去可以去掉的也就是 (j) 了,可以思考一下 (j) 到底有什么用:在经过 (j) 时,可以放置一个传送门,可能会节省后面的时间。再进一步,当 (j) 位置的传送门有用的时候,不就满足 (j=x) 吗?我们完全可以用某个点到 (j) 的最短路来计算这次放置传送门的费用。
于是令 (dp_{i,x}) 为经过前 (i) 个必经点,传送门是 (x) 的答案。它可以由 (dp_{i-1,y}) 转移过来,这就是一个 (mathcal O(n^3)) 的复杂度。具体有三种情况:
- 从 (a_{i-1}) 直接走到 (a_i)。即 (dp_{i,x}leftarrow dp_{i-1,x})。
- 从 (a_{i-1}) 瞬移到 (y),从 (y) 走到 (x) 放置传送门,从 (x) 走到 (a_i)。
- 从 (a_{i-1}) 走到 (x) 并放置传送门,从 (x) 瞬移到 (y),从 (y) 走到 (a_i)。
不过你可能会疑惑,为什么不判断从 (a_{i-1}) 走到 (x) 并放置传送门,再走到 (a_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 <iostream>
using namespace std;
typedef long long ll;
const int maxn=305;
int n,m,k,a[maxn<<1];
ll d[maxn][maxn],dp[maxn<<1][maxn];
signed main() {
n=read(9),m=read(9),k=read(9);
for(int i=1;i<=n;++i)
for(int j=i+1;j<=n;++j)
d[j][i]=d[i][j]=1e15;
for(int i=1;i<=m;++i) {
int u,v,w;
u=read(9),v=read(9),w=read(9);
d[u][v]=d[v][u]=min(0ll+w,d[u][v]);
}
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
for(int i=1;i<=k;++i)
a[i<<1]=read(9),a[(i<<1)+1]=read(9);
for(int i=1;i<=n;++i)
dp[1][i]=d[1][i];
a[1]=1;
for(int i=2;i<=(k<<1)+1;++i) {
for(int j=1;j<=n;++j) {
dp[i][j]=dp[i-1][j]+d[a[i-1]][a[i]];
for(int k=1;k<=n;++k) {
dp[i][j]=min(dp[i][j],dp[i-1][k]+d[k][j]+d[j][a[i]]);
dp[i][j]=min(dp[i][j],dp[i-1][k]+d[a[i-1]][j]+d[k][a[i]]);
}
}
}
ll ans=1e15;
for(int i=1;i<=n;++i)
ans=min(ans,dp[(k<<1)+1][i]);
print(ans,'
');
return 0;
}