前言:
本篇文章为下期(收录题目 O–>Z \text{O-->Z} O–>Z),题目比较有思维难度,值得一做。
O题
观察题目数据范围,允许状态压缩。
设 f i , s f_{i,s} fi,s 表示右部中的点,与左部中 i i i 个点构成匹配的集合为 s s s 的方案数,答案为 f n , ( 1 < < n ) − 1 f_{n,(1<<n)-1} fn,(1<<n)−1。
考虑如何转移,当构成匹配集合为 s s s 时,我们发现二进制表示下 1 1 1 的数量(即当前匹配数),应当等于 i i i,这样才能更新。
找到一个右部的一个点,满足它不属于集合 s s s,并且能与当前枚举的左部点匹配,这样就可以进行更新。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=22,M=(1<<N),mod=1e9+7;
int n,a[N][N],f[M];
int popcount(int x){int res=0;while(x){if(x&1)res++;x>>=1;}return res;}
int main(){
n=rd;FOR(i,0,n-1) FOR(j,0,n-1) a[i][j]=rd;
f[0]=1;
FOR(s,0,(1<<n)-1){
int cnt=popcount(s);
FOR(i,0,n-1) if(a[cnt][i]&&(s&(1<<i))==0) f[s|(1<<i)]=(1ll*f[s|(1<<i)]+f[s])%mod;
}
printf("%d\n",f[(1<<n)-1]);
return 0;
}
P题
基础树形 dp \text{dp} dp。
设 f x , 0 / 1 f_{x,0/1} fx,0/1 表示以 x x x 为根的子树中, x x x 号节点染白 / / /黑色的方案数。
设边 ( x , y ) (x,y) (x,y),为满足相邻两点不全为黑色的限制,转移为:
- f x , 0 = f x , 0 × ( f y , 0 + f y , 1 ) f_{x,0}=f_{x,0}\times(f_{y,0}+f_{y,1}) fx,0=fx,0×(fy,0+fy,1)。
- f x , 1 = f x , 1 × f y , 0 f_{x,1}=f_{x,1}\times f_{y,0} fx,1=fx,1×fy,0。
设 1 1 1 为根,答案为 f 1 , 0 + f 1 , 1 f_{1,0}+f_{1,1} f1,0+f1,1。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=1e5+10,mod=1e9+7;
int n,head[N],tot,f[N][2];
struct node{int to,nxt;}edge[N<<1];
void add(int x,int y){edge[++tot].to=y,edge[tot].nxt=head[x],head[x]=tot;}
void dfs(int x,int fa){
f[x][0]=f[x][1]=1;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;if(y==fa) continue;
dfs(y,x);
f[x][0]=1ll*f[x][0]*(f[y][0]+f[y][1])%mod;
f[x][1]=1ll*f[x][1]*f[y][0]%mod;
}
}
int main(){
n=rd;
FOR(i,1,n-1){int x=rd,y=rd;add(x,y),add(y,x);}
dfs(1,0);
printf("%d\n",(1ll*f[1][0]+f[1][1])%mod);
return 0;
}
Q题
状态设计和转移方程是很显然的,设 f i f_{i} fi 表示前 i i i 朵花,选了第 i i i 朵花能得到的最大权值。
转移为 f i = max j = 0 i − 1 { f j } + a i , h j < h i f_{i}=\max_{j=0}^{i-1}\{f_j\}+a_i,h_j<h_i fi=maxj=0i−1{fj}+ai,hj<hi。
暴力做是 O ( n 2 ) O(n^2) O(n2) 的,考虑优化。
我们发现每次查找的是前面所有满足 h j < h i h_j<h_i hj<hi 的 f j f_j fj 的最大值,所以可以以 h h h 作为下标,建立权值线段树,维护区间中 f f f 最大值。每次查找的都是一段前缀,计算完后还要更新 f i f_i fi。
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=2e5+10;
int n,h[N],a[N];ll mx[N<<2];
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int x,ll v){
if(l==r){mx[u]=v;return;}
int mid=(l+r)>>1;
if(x<=mid) modify(u<<1,l,mid,x,v);
else modify(u<<1|1,mid+1,r,x,v);
pushup(u);
}
ll query(int u,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return mx[u];
int mid=(l+r)>>1;ll ans=0;
if(ql<=mid) ans=max(ans,query(u<<1,l,mid,ql,qr));
if(qr>mid) ans=max(ans,query(u<<1|1,mid+1,r,ql,qr));
return ans;
}
int main(){
n=rd;ll ans=0;
FOR(i,1,n) h[i]=rd;
FOR(i,1,n) a[i]=rd;
FOR(i,1,n){
ll tmp=query(1,1,n,1,h[i])+a[i];
ans=max(ans,tmp),modify(1,1,n,h[i],tmp);
}
printf("%lld\n",ans);
return 0;
}
R题
矩阵加速 dp \text{dp} dp。
比较 naive \text{naive} naive 的 dp \text{dp} dp 很好想,设 f t , i , j f_{t,i,j} ft,i,j 表示从 i → j i\to j i→j,经过路径长度为 t t t 的方案数,我们可以枚举中间某个点 k k k,则 f t , i , j = ∑ k = 1 n f t − 1 , i , k × f 1 , k , j f_{t,i,j}=\sum_{k=1}^{n}f_{t-1,i,k}\times f_{1,k,j} ft,i,j=∑k=1nft−1,i,k×f1,k,j。
我们发现这和矩阵乘法的式子一模一样,而 f 1 f_1 f1 矩阵即为题中所给,而 f t = f t − 1 × f 1 f_t=f_{t-1}\times f_1 ft=ft−1×f1,所以最终的答案矩阵为 f k = f 1 k f_k=f_1^k fk=f1k。
最后就是一个矩阵加速幂的模版了,不会的可以学一学。
时间复杂度 O ( n 3 l o g k ) O(n^3logk) O(n3logk)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=55,mod=1e9+7;
int n;ll K;
struct matrix{
int a[N][N];
matrix(){memset(a,0,sizeof(a));}
matrix operator*(const matrix&T){
matrix res;
FOR(i,0,n-1){
FOR(k,0,n-1){
int r=a[i][k];
FOR(j,0,n-1)
res.a[i][j]=(1ll*res.a[i][j]+1ll*r*T.a[k][j]%mod)%mod;
}
}
return res;
}
}M,ans;
int main(){
n=rd,scanf("%lld",&K);
FOR(i,0,n-1) ans.a[i][i]=1;
FOR(i,0,n-1) FOR(j,0,n-1) M.a[i][j]=rd;
while(K){
if(K&1) ans=ans*M;
M=M*M,K>>=1;
}
int res=0;
FOR(i,0,n-1) FOR(j,0,n-1) res=(1ll*res+ans.a[i][j])%mod;
cout<<res<<endl;
return 0;
}
S题
蛮套路的数位 dp \text{dp} dp。
我们考虑记忆化搜索来写,从高位到低位枚举,记录当前所有数码和模
D
D
D 的值,注意判断是否有前导零。
然后写法就和其他记搜数位
dp
\text{dp}
dp 一样,看代码就能理解了。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=10010,M=110,mod=1e9+7;
int n,cnt,f[N][M][2],d,num[N];char ch[N];
int dfs(int pos,int sum,bool zero,bool lim){
if(!pos) return sum==0&&!zero;//枚举完了
if(!lim&&~f[pos][sum][zero]) return f[pos][sum][zero];//记搜
int up=lim?num[pos]:9,res=0;//是否有最高位的限制
FOR(i,0,up) res=(1ll*res+dfs(pos-1,(sum+i)%d,zero&&i==0,lim&&i==up))%mod;
if(!lim) f[pos][sum][zero]=res;
return res;
}
int main(){
scanf("%s",ch+1),n=strlen(ch+1),d=rd;
ROF(i,n,1) num[++cnt]=ch[i]-'0';
memset(f,-1,sizeof(f)),printf("%d\n",dfs(cnt,0,1,1));
return 0;
}
T题
这个题非常的妙!
一开始很容易想到 f i , j f_{i,j} fi,j 表示前 i i i 个数,第 i i i 位填 j j j 的方案数,然后 j j j 从 1 1 1 到 n n n 枚举,你会发现答案错了。为什么?因为你很容易把不合法的方案给统计上,会算重。
那么怎么做,我们发现其实只关心大小关系,比如 4 , 5 , 6 4,5,6 4,5,6,完全可以映射为 1 , 2 , 3 1,2,3 1,2,3,所以我们规定前 i i i 个数中,就只从 1 → i 1\to i 1→i 中添数。所以 j j j 从 1 → i 1\to i 1→i 枚举,对于转移:
- f i , j = ∑ k = 1 j − 1 f i − 1 , k , (if op=<) f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k},\text{(if op=<)} fi,j=∑k=1j−1fi−1,k,(if op=<)
- f i , j = ∑ k = j i − 1 f i − 1 , k , (if op=>) f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k},\text{(if op=>)} fi,j=∑k=ji−1fi−1,k,(if op=>)
你可能会疑惑第二个式子,因为对于 > > >,我们可以将 [ 1 , i − 1 ] [1,i-1] [1,i−1] 中 > j >j >j 的数都加上 1 1 1,这样能保证值域为 [ 1 , i ] [1,i] [1,i],且大小关系不变,并且可以从 i − 1 i-1 i−1 转移。
转移可以用前缀和优化,时间复杂度 O ( n 2 ) O(n^2) O(n2)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=3010,mod=1e9+7;
int n,f[N][N],s[N][N];char ch[N];
int main(){
n=rd,scanf("%s",ch+1);
f[1][1]=s[1][1]=1;
FOR(i,2,n) FOR(j,1,i){
if(ch[i-1]=='<') f[i][j]=s[i-1][j-1];
else f[i][j]=(1ll*s[i-1][i-1]-s[i-1][j-1]+mod)%mod;
s[i][j]=(1ll*s[i][j-1]+f[i][j])%mod;
}
printf("%d\n",s[n][n]);
return 0;
}
U题
设 f S f_S fS 表示当前所选物品集合为 S S S 时所得的最大价值,转移为 f S = max { f S ′ + w S ⊕ S ′ } f_S=\max \{\ f_{S'}+w_{S\oplus S'}\} fS=max{ fS′+wS⊕S′}( ⊕ \oplus ⊕ 为异或),枚举 S ′ ⊆ S S'\subseteq S S′⊆S 即可完成转移。
w S w_S wS 为将集合 S S S 中的数放入同一组中的价值,这个可以 O ( 2 n n 2 ) O(2^nn^2) O(2nn2) 预处理,然后转移 f f f 时枚举子集,最终复杂度为 O ( 2 n n 2 + 3 n ) O(2^nn^2+3^n) O(2nn2+3n)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=16,M=(1<<N)+10;
int n,a[N][N];ll f[M],w[M];
int main(){
n=rd;
FOR(i,0,n-1) FOR(j,0,n-1) a[i][j]=rd;
FOR(i,0,(1<<n)-1){
ll res=0;
FOR(j,0,n-1) FOR(k,j+1,n-1){
if(((i>>j)&1)&&((i>>k)&1)) res+=a[j][k];
}
f[i]=w[i]=res;
}
FOR(i,0,(1<<n)-1){
for(int s=i;s;s=(s-1)&i) f[i]=max(f[i],f[i^s]+w[s]);
}
printf("%lld\n",f[(1<<n)-1]);
return 0;
}
V题
树形 dp \text{dp} dp 好题。
要求所有节点的答案,考虑换根 dp \text{dp} dp,先想根节点确定的做法。
设 f x f_x fx 表示以 x x x 为根的子树中, x x x 染成黑色所得到的连通块的方案数。
设 y y y 为 x x x 的儿子,转移为 f x ← f x × ( f y + 1 ) f_x\gets f_x\times (f_y+1) fx←fx×(fy+1),表示 y y y 可以染黑色或白色。
再考虑换根 dp \text{dp} dp,设 g x g_x gx 表示以 x x x 为根节点的子树外,染成黑色的节点与 x x x 构成一个连通块的方案数,考虑 f a fa fa 与它的兄弟的贡献: g x = g f a ∏ v ( f v + 1 ) + 1 , v ∈ brother(x) g_x=g_{fa}\prod_{v}(f_v+1)+1,v\in \text{brother(x)} gx=gfa∏v(fv+1)+1,v∈brother(x)。
暴力求是 O ( n 2 ) O(n^2) O(n2),可以在第一遍 dfs \text{dfs} dfs 时求出 f f f 的前缀积、后缀积,就可以做到 O ( n ) O(n) O(n) 了。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
#define debug cout<<"I love CCF"<<endl;
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=1e5+10;
int n,mod,head[N],tot,f[N],g[N],pre[N],nxt[N];
struct node{int to,nxt;}edge[N<<1];
void add(int x,int y){edge[++tot]={y,head[x]},head[x]=tot;}
void dfs(int x,int fa){
f[x]=1;vector<int> v;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;if(y==fa) continue;
dfs(y,x);
f[x]=1ll*f[x]*(f[y]+1)%mod;
v.push_back(y);
}
int res=1;
if(!v.size()) return;
for(int i=0;i<=v.size()-1;i++){
pre[v[i]]=res,res=1ll*res*(f[v[i]]+1)%mod;
}
res=1;
for(int i=v.size()-1;i>=0;i--){
nxt[v[i]]=res,res=1ll*res*(f[v[i]]+1)%mod;
}
}
void DFS(int x,int fa){
if(fa==0) g[x]=1;
else g[x]=(1ll*g[fa]*pre[x]%mod*nxt[x]%mod+1)%mod;
for(int i=head[x];i;i=edge[i].nxt){
int y=edge[i].to;if(y==fa) continue;
DFS(y,x);
}
}
int main(){
n=rd,mod=rd;
FOR(i,1,n-1){int x=rd,y=rd;add(x,y),add(y,x);}
dfs(1,0),DFS(1,0);
FOR(i,1,n){
int ans=1ll*f[i]*g[i]%mod;
printf("%d\n",ans);
}
return 0;
}
W题
区间贡献,直接做是很难转移的,所以把区间的右端点记下来,我们只在到达右端点时才更新这段区间,这样可以保证不重不漏。
令 f i , j f_{i,j} fi,j 表示前 i i i 个位置,上一个 1 1 1 在 j j j 位置得到的最大价值,显然转移有:
f i , i = max j = 1 i − 1 f i − 1 , j + ∑ l k ≤ i ∧ r k = i a k \begin{array}{c} &&&&&&&&&&f_{i,i}=\max_{j=1}^{i-1}f_{i-1,j}+\sum_{l_k\le i\wedge r_k=i}a_k \end{array} fi,i=maxj=1i−1fi−1,j+∑lk≤i∧rk=iak
f i , j = f i − 1 , j + ∑ l k ≤ j ∧ r k = i a k \begin{array}{c} &&&&&&&&&&f_{i,j}&=f_{i-1,j}+\sum_{l_k\le j\wedge r_k=i}a_k\\ \end{array} fi,j=fi−1,j+∑lk≤j∧rk=iak
于是我们就有了 O ( n 2 ) O(n^2) O(n2) 的做法,滚动数组优化一下,空间复杂度为 O ( n ) O(n) O(n)。
思考优化,我们发现对于 i = j i=j i=j,实际就是在 [ 1 , i − 1 ] [1,i-1] [1,i−1] 查询了 f f f 的最大值,然后对于所有 r = i r=i r=i 的区间,它们都会对 [ l , r ] [l,r] [l,r] 内的 dp \text{dp} dp 值产生 a a a 的贡献。
所以我们用线段树维护 f f f,每到一个新位置执行两种操作:
- 查询全局最大值,更新 i i i 位置。
- 对于所有 r = i r=i r=i 的区间,在 [ l , r ] [l,r] [l,r] 区间内加上 a a a。
区间加、区间最大值,线段树维护就好了,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PII pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=2e5+10;
int n,m;ll mx[N<<2],tag[N<<2];
vector<PII> d[N];
void pushdown(int u){
tag[u<<1]+=tag[u],tag[u<<1|1]+=tag[u];
mx[u<<1]+=tag[u],mx[u<<1|1]+=tag[u];
tag[u]=0;
}
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int ql,int qr,ll v){
if(ql<=l&&r<=qr){
mx[u]+=v,tag[u]+=v;
return;
}
pushdown(u);
int mid=(l+r)>>1;
if(ql<=mid) modify(u<<1,l,mid,ql,qr,v);
if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,v);
pushup(u);
}
int main(){
n=rd,m=rd;
FOR(i,1,m){int l=rd,r=rd,v=rd;d[r].pb(mp(l,v));}
FOR(i,1,n){
modify(1,1,n,i,i,max(0ll,mx[1]));
for(auto j:d[i]) modify(1,1,n,j.fi,i,(ll)j.se);
}
printf("%lld\n",max(mx[1],0ll));
return 0;
}
X题
贪心 + + + dp \text{dp} dp 的好题。
有重量、体积、价值的关键字眼,考虑 01 01 01 背包,但发现暴力做是 O ( n 2 w ) O(n^2w) O(n2w) 的。
考虑贪心,对于两个物品 i , j i,j i,j,假如先放 i i i,后放 j j j,则 i i i 后面还能放 s i − w j s_i-w_j si−wj 重量的物品,反之则为 s j − w i s_j-w_i sj−wi 的物品。若先放 i i i,则应满足 s i − w j > s j − w i s_i-w_j>s_j-w_i si−wj>sj−wi,即 s i + w i > s j + w j s_i+w_i>s_j+w_j si+wi>sj+wj。所以按 s + w s+w s+w 从小到大排序,然后从上往下放,进行 01 01 01 背包的转移。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=10010;
int n;ll f[N<<2];
struct node{int w,s,v;}a[N];
bool cmp(node a,node b){return a.w+a.s<b.w+b.s;}
int main(){
n=rd;memset(f,-1,sizeof(f)),f[0]=0;
FOR(i,1,n) a[i].w=rd,a[i].s=rd,a[i].v=rd;
sort(a+1,a+1+n,cmp);
FOR(i,1,n) ROF(j,a[i].s,0){//i上面放了j的物品,现在加上i
f[j+a[i].w]=max(f[j+a[i].w],f[j]+a[i].v);
}
ll ans=0;
FOR(i,0,20000) ans=max(ans,f[i]);
printf("%lld\n",ans);
return 0;
}
Y题
经典计数题。
正难则反,考虑容斥思想。首先不考虑限制,从 ( 1 , 1 ) → ( n , m ) (1,1)\to (n,m) (1,1)→(n,m) 方案数为 ( n + m − 2 n − 1 ) \begin{pmatrix}n+m-2\\n-1\end{pmatrix} (n+m−2n−1),然后减去经过限制点的方案数。
如何减去经过限制点的方案数呢?如果用朴素的容斥是 2 2 2 的次方级别的,复杂度无法接受,并且太麻烦了。
所以考虑 dp \text{dp} dp,设 f i f_i fi 表示从 ( 1 , 1 ) (1,1) (1,1) 到第 i i i 个限制点,中间不经过其他限制点的方案数。
我们先按坐标排序,然后进行转移: f i = ( x i + y i − 2 x i − 1 ) − ∑ j = 1 i − 1 f j × ( x i − x j + y i − y j x i − x j ) f_i=\begin{pmatrix}x_i+y_i-2\\x_i-1\end{pmatrix}-\sum_{j=1}^{i-1}f_j\times \begin{pmatrix}x_i-x_j+y_i-y_j\\x_i-x_j\end{pmatrix} fi=(xi+yi−2xi−1)−∑j=1i−1fj×(xi−xj+yi−yjxi−xj)。
我们发现这样可以不重不漏的统计从 ( 1 , 1 ) → ( x i , y i ) (1,1)\to (x_i,y_i) (1,1)→(xi,yi) 的方案数。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=3010,M=2e5+10,mod=1e9+7;
int n,m,r,fac[M],infac[M],f[N];
struct node{int x,y;}a[N];
bool cmp(node a,node b){
if(a.x==b.x) return a.y<b.y;
return a.x<b.x;
}
int qpow(int a,int b){
int res=1;
while(b){if(b&1)res=1ll*res*a%mod;a=1ll*a*a%mod,b>>=1;}
return res;
}
void init(){
int t=2e5;fac[0]=1;
FOR(i,1,t) fac[i]=1ll*fac[i-1]*i%mod;
infac[t]=qpow(fac[t],mod-2);
ROF(i,t-1,0) infac[i]=1ll*infac[i+1]*(i+1)%mod;
}
int C(int n,int m){
if(n<0||m<0||n<m) return 0;
return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
int main(){
init();
n=rd,m=rd,r=rd;FOR(i,1,r) a[i].x=rd,a[i].y=rd;
r++,a[r]={n,m};
sort(a+1,a+1+r,cmp);
FOR(i,1,r){
f[i]=C(a[i].x+a[i].y-2,a[i].x-1);
FOR(j,1,i-1){
f[i]=(1ll*f[i]-1ll*f[j]*C(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod+mod)%mod;
}
}
printf("%d\n",f[r]);
return 0;
}
Z题
终于到最后一道题了,实际上就是一道斜率优化板子题。
朴素转移方程很好写: f i = min j = 1 i − 1 { f j + ( h i − h j ) 2 + C } f_i=\min_{j=1}^{i-1}\{f_j+(h_i-h_j)^2+C\} fi=minj=1i−1{fj+(hi−hj)2+C},暴力做是 O ( n 2 ) O(n^2) O(n2) 的。
把 min \min min 里面的式子拆开:
f j + ( h i − h j ) 2 + C = f j + h i 2 + h j 2 − 2 h i h j + C = ( f j + h j 2 ) − 2 h j h i + C + h i 2 \begin{aligned} f_j+(h_i-h_j)^2+C&=f_j+h_i^2+h_j^2-2h_ih_j+C \\&=(f_j+h_j^2)-2h_jh_i+C+h_i^2 \end{aligned} fj+(hi−hj)2+C=fj+hi2+hj2−2hihj+C=(fj+hj2)−2hjhi+C+hi2
所以令
F
j
=
−
2
h
j
x
+
h
j
2
+
f
j
F_j=-2h_jx+h_j^2+f_j
Fj=−2hjx+hj2+fj,直接李超线段树,斜率优化啥的就滚远吧!
对于 i i i,我们就找到 x = h i x=h_i x=hi 时纵坐标最小的点,然后进行更新即可。
时间复杂度 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)。
如果你不会李超线段树,或者想追求更优的时间复杂度的话,由于本题 h h h 单调递增的特殊性质,还有 O ( n ) O(n) O(n) 的斜率优化做法。
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PDI pair<double,int>
#define mp make_pair
#define int long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=1e6+10,M=1e6;
const double eps=1e-10;
int n,h[N],seg[N<<2],C,f[N];
struct node{double k,b;}p[N];
int cmp(double x,double y){
if(x-y>eps) return -1;
if(y-x>eps) return 1;
return 0;
}
double cal(int id,int x){
return p[id].k*x+p[id].b;
}
void update(int u,int l,int r,int id){
int &g=seg[u],mid=(l+r)>>1;
if(cmp(cal(id,mid),cal(g,mid))==1) swap(g,id);
if(cmp(cal(id,l),cal(g,l))==1) update(u<<1,l,mid,id);
if(cmp(cal(id,r),cal(g,r))==1) update(u<<1|1,mid+1,r,id);
}
void modify(int u,int l,int r,int ql,int qr,int id){
if(ql<=l&&r<=qr){update(u,l,r,id);return;}
int mid=(l+r)>>1;
if(ql<=mid) modify(u<<1,l,mid,ql,qr,id);
if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,id);
}
PDI pmx(PDI a,PDI b){
if(cmp(a.first,b.first)>0) return a;
else return b;
}
PDI query(int u,int l,int r,int v){
if(l>v||r<v) return mp(0,0);
int mid=(l+r)>>1;PDI res=mp(cal(seg[u],v),seg[u]);
if(l==r) return res;
return pmx(res,pmx(query(u<<1,l,mid,v),query(u<<1|1,mid+1,r,v)));
}
signed main(){
n=rd,C=rd;
FOR(i,1,n) h[i]=rd;
p[0]={0,1e16};
p[1]={-2.0*h[1],1.0*h[1]*h[1]};
modify(1,1,M,1,M,1);
FOR(i,2,n){
int t=query(1,1,M,h[i]).second;
f[i]=p[t].k*h[i]+p[t].b+C+h[i]*h[i];
p[i]={-2.0*h[i],1.0*h[i]*h[i]+f[i]};
modify(1,1,M,1,M,i);
}
printf("%lld\n",f[n]);
return 0;
}