A - Frog 1
線性 DP。
狀態轉移方程為
,注意邊界。
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int n,a[N];
int f[N];
inline int ABS(int x){return max(x,-x);}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
f[2]=ABS(a[2]-a[1]);
for(int i=3;i<=n;i++)
f[i]=min(f[i-1]+ABS(a[i]-a[i-1]),f[i-2]+ABS(a[i]-a[i-2]));
cout<<f[n]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
B - Frog 2
線性 DP。
狀態轉移方程為
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e5+5;
int n,m,a[N];
inline int ABS(int x){return max(x,-x);}
int f[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
memset(f,0x3f,sizeof(f));
f[1]=0;
for(int i=2;i<=n;i++)
for(int j=max(1,i-m);j<i;j++)
f[i]=min(f[i],f[j]+ABS(a[i]-a[j]));
cout<<f[n]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
C - Vacation
線性 DP。
設 \(f_{i,0/1/2}\) 表示前 \(i\) 天,第 \(i\) 天選 A,B,C 的最大幸福值,轉移方程為
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int n,a[N],b[N],c[N];
int f[N][3];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>c[i];
for(int i=1;i<=n;i++){
f[i][0]=max(f[i-1][1],f[i-1][2])+a[i];
f[i][1]=max(f[i-1][0],f[i-1][2])+b[i];
f[i][2]=max(f[i-1][0],f[i-1][1])+c[i];
}
cout<<max(max(f[n][0],f[n][1]),f[n][2])<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
D - Knapsack 1
揹包 DP。
設 \(f_{i,j}\) 為前 \(i\) 個物品,揹包容量為 \(j\) 的最大價值,狀態轉移方程為
使用滾動陣列或者一種廣為人知的動態更新可以做到 \(O(W)\) 空間。
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=1e5+5;
int n,m;
ll f[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1,v,w;i<=n;i++){
cin>>w>>v;
for(int j=m;j>=w;j--)f[j]=max(f[j],f[j-w]+v);
}
cout<<f[m]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
E - Knapsack 2
揹包 DP。
設 \(f_{i,j}\) 為前 \(i\) 個物品,價值為 \(j\) 的最小容量,狀態轉移方程為
同樣可以用滾動陣列或者一種廣為人知的動態更新可以做到 \(O(Nv)\) 空間。
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
using ll=long long;
const int N=1e5+5;
int n,m;
ll f[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=1,w,v;i<=n;i++){
cin>>w>>v;
for(int j=100000;j>=v;j--)f[j]=min(f[j],f[j-v]+w);
}
for(int i=100000;~i;i--)
if(f[i]<=m)return cout<<i<<endl,0;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
F - LCS
經典題。
設 \(f_{i,j}\) 為 \(s[1..i]\) 與 \(t[1..j]\) 的 LCS 長度,則轉移方程為
記錄轉移以輸出方案。
程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
using pii=pair<int,int>;
const int N=3005;
int n,m;
string s,t;
int f[N][N];
pii pa[N][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>s>>t;n=s.size(),m=t.size();s=' '+s,t=' '+t;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(f[i][j-1]>f[i-1][j])f[i][j]=f[i][j-1],pa[i][j]={i,j-1};
else f[i][j]=f[i-1][j],pa[i][j]={i-1,j};
if(f[i-1][j-1]+(s[i]==t[j])>f[i][j])f[i][j]=f[i-1][j-1]+(s[i]==t[j]),pa[i][j]={i-1,j-1};
}
string ans;
pii p={n,m};
while(p.first&&p.second){
pii ne=pa[p.first][p.second];
if(ne.first==p.first-1&&ne.second==p.second-1&&s[p.first]==t[p.second])ans+=s[p.first];
p=ne;
}
reverse(ans.begin(),ans.end());
cout<<ans<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
G - Longest Path
DAG 上 DP。
狀態轉移方程為
程式碼
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
const int N=1e5+5;
int n,m;
vector<int>gr[N];
int ind[N];
int f[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
gr[u].push_back(v),ind[v]++;
}
queue<int>q;
for(int i=1;i<=n;i++)if(!ind[i])q.push(i);
while(q.size()){
int p=q.front();
q.pop();
for(auto to:gr[p]){
f[to]=max(f[to],f[p]+1);
if(!--ind[to])q.push(to);
}
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,f[i]);
cout<<ans<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
H - Grid 1
狀態轉移方程為
程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=1005;
int n,m;
char ch[N][N];
ll f[N][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>ch[i][j];
f[1][1]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(ch[i][j]=='.')(f[i][j]+=f[i-1][j]+f[i][j-1])%=mod;
cout<<f[n][m]<<endl;
return 0;
}
I - Coins
期望 DP。
設 \(f_{i,j}\) 為前 \(i\) 個硬幣,有 \(j\) 個正面的機率,轉移方程為
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
const int N=3005;
int n;db a[N];
db f[2][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int cur=0;
f[0][0]=1;
for(int i=1;i<=n;i++){
cur^=1;
for(int j=0;j<=i;j++)f[cur][j]=0;
f[cur][0]=f[cur^1][0]*(1-a[i]);
for(int j=1;j<=i;j++)f[cur][j]+=f[cur^1][j]*(1-a[i])+f[cur^1][j-1]*a[i];
}
db ans=0;
for(int i=n/2+1;i<=n;i++)ans+=f[cur][i];
printf("%.9lf\n",ans);
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
J - Sushi
期望 DP。
設 \(f_{a,b,c}\) 為當前有 \(a\) 個盤子裝 \(1\) 個,\(b\) 個盤子裝 \(2\) 個,\(c\) 個盤子裝 \(3\) 個,期望還要操作幾次,可以得出
整理得
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
const int N=305;
int n,a,b,c;
db f[N][N][N];
db dfs(int a,int b,int c){
if(!a&&!b&&!c)return 0;
if(a<0||b<0||c<0)return 0;
if(f[a][b][c]!=-1)return f[a][b][c];
db&ret=f[a][b][c];ret=0;
ret=((db)a*dfs(a-1,b,c)+(db)b*dfs(a+1,b-1,c)+(db)c*dfs(a,b+1,c-1)+n)/(db)(a+b+c);
return ret;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1,x;i<=n;i++){
cin>>x;
if(x==1)a++;
else if(x==2)b++;
else if(x==3)c++;
}
for(int i=0;i<=n;i++)
for(int j=0;j<=n;j++)
for(int k=0;k<=n;k++)
f[i][j][k]=-1;
printf("%.9lf\n",dfs(a,b,c));
return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing
K - Stones
博弈論。
設 \(f_i = 0/1\) 為當前狀態先手必敗 / 必勝,則有
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e5+5;
int n,k,a[N];
int f[N];
bool dfs(int k){
if(f[k]!=-1)return f[k];
f[k]=0;
for(int i=1;i<=n;i++)
if(k>=a[i])f[k]|=!dfs(k-a[i]);
return f[k];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>a[i];
memset(f,-1,sizeof(f));
if(dfs(k))cout<<"First"<<endl;
else cout<<"Second"<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/14
//When I wrote a piece of code, her and I knew what it was doing
L - Deque
博弈論。
設 \(f_{l,r}\) 為當前序列為 \(a_l \ldots a_r\) 時的結果,轉移方程為
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
using ll=long long;
const int N=3005;
int n,a[N];
ll f[N][N];
inline ll dfs(int l,int r){
if(l==r)return a[l];
if(l>r)return 0;
if(f[l][r]!=-1)return f[l][r];
return f[l][r]=max(a[l]-dfs(l+1,r),a[r]-dfs(l,r-1));
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
memset(f,-1,sizeof(f));
cout<<dfs(1,n)<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing
M - Candies
字首和最佳化 DP。
設 \(f_{i,j}\) 為前 \(i\) 個孩子,分了 \(j\) 個糖果的方案數,有轉移
易用字首和最佳化至 \(O(NK)\) 時間複雜度。
程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=105,M=1e5+5;
int n,m,a[N];
int f[2][M],g[M];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
f[0][0]=1;
int cur=0;
for(int i=1;i<=n;i++){
g[0]=f[cur][0];
for(int j=1;j<=m;j++)g[j]=(g[j-1]+f[cur][j])%mod;
cur^=1;
for(int j=0;j<=m;j++)f[cur][j]=0;
for(int j=0;j<=m;j++)f[cur][j]=((g[j]-(j-a[i]<=0?0:g[j-a[i]-1]))%mod+mod)%mod;
}
cout<<f[cur][m]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing
N - Slimes
區間 DP。
設 $ f_{l,r} $ 為區間 $ [l,r] $ 的最小代價,則轉移方程為
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
using ll=long long;
const int N=405;
const ll inf=1e16+5;
int n,a[N];ll s[N];
ll f[N][N];
ll dfs(int l,int r){
if(l>=r)return 0;
if(f[l][r]!=-1)return f[l][r];
ll&ret=f[l][r];ret=inf;
for(int i=l;i<r;i++)
ret=min(ret,dfs(l,i)+dfs(i+1,r)+s[r]-s[l-1]);
return ret;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],s[i]=s[i-1]+a[i];
memset(f,-1,sizeof(f));
cout<<dfs(1,n)<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing
O - Matching
狀壓 DP。
設 \(f_{S,i}\) 為匹配前 \(i\) 個男性,女性匹配情況為 \(S\) 的方案數,可得轉移方程
程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=21;
int n,a[N][N];
ll f[1<<N][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>a[i][j];
for(int i=0;i<n;i++)if(a[0][i])f[1<<i][0]=1;
for(int i=0;i<n-1;i++)
for(int S=0;S<(1<<n);S++){
if(f[S][i]==0)continue;
for(int j=0;j<n;j++){
if((S>>j)&1||!a[i+1][j])continue;
(f[S|(1<<j)][i+1]+=f[S][i])%=mod;
}
}
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing
P - Independent Set
樹上 DP。
設 \(f_{u,0/1}\) 為以第 \(u\) 個點為根的子樹,點 \(u\) 顏色為白 / 黑的方案數,轉移方程為
程式碼
#include<iostream>
#include<cstdio>
#include<vector>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=1e5+5;
int n;
vector<int>gr[N];
ll f[N][2];
void dfs(int p,int fa){
f[p][0]=f[p][1]=1;
for(auto to:gr[p]){
if(to==fa)continue;
dfs(to,p);
(f[p][0]*=f[to][0]+f[to][1])%=mod;
(f[p][1]*=f[to][0])%=mod;
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
gr[u].push_back(v),gr[v].push_back(u);
}
dfs(1,0);
cout<<(f[1][0]+f[1][1])%mod<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing
Q - Flowers
資料結構最佳化 DP。
設 \(f_{i}\) 為前 \(i\) 朵花,且選擇第 \(i\) 朵花的美麗值之和的最大值,有轉移
注意到這相當於一個二維偏序,因此可以用 BIT 最佳化至線性對數時間複雜度。
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=2e5+5;
int n,a[N],h[N];
ll f[N];
ll t[N];
inline void update(int i,ll x){for(i++;i<=n+1;i+=i&-i)t[i]=max(t[i],x);}
inline ll query(int i){ll ret=0;for(i++;i;i-=i&-i)ret=max(ret,t[i]);return ret;}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>h[i];
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
f[i]=query(h[i]-1)+a[i];
update(h[i],f[i]);
}
cout<<query(n)<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing
R - Walk
矩陣乘法加速 DP。
設 \(f_{i,j}\) 為走了 \(i\) 步,當前在點 \(j\) 的方案數,有轉移
可以將轉移寫成矩陣乘法的形式,做到 \(O(n^3 \log k)\) 。
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=55;
int n;ll m;
struct matrix{
ll a[N][N];
int n,m;
matrix(){
n=m=0;
memset(a,0,sizeof(a));
}
friend matrix operator*(matrix a,matrix b){
matrix c;c.n=a.n,c.m=b.m;
for(int k=1;k<=a.m;k++)
for(int i=1;i<=a.n;i++)
for(int j=1;j<=b.m;j++)
(c.a[i][j]+=a.a[i][k]*b.a[k][j])%=mod;
return c;
}
};
inline matrix qpow(matrix a,ll p){
matrix ret;ret.n=a.n,ret.m=a.m;
for(int i=1;i<=ret.n;i++)ret.a[i][i]=1;
while(p){
if(p&1)ret=ret*a;
a=a*a,p>>=1;
}
return ret;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
matrix I,C;C.n=n,C.m=n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>C.a[i][j];
I.n=1,I.m=n;
for(int i=1;i<=n;i++)I.a[1][i]=1;
C=qpow(C,m);
I=I*C;
ll ans=0;
for(int i=1;i<=n;i++)(ans+=I.a[1][i])%=mod;
cout<<ans<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing
S - Digit Sum
數位 DP。
設 \(f_{i,j}\) 為還有 \(i\) 位未填,數位之和模 \(D\) 等於 \(j\) 的方案數,有轉移
程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=1e4+5;
int n;string s;
ll f[N][105];
int num[N];
ll dfs(int p,int s,bool limit){
if(!p)return s==0;
if(!limit&&f[p][s]!=-1)return f[p][s];
ll ret=0;int up=limit?num[p]:9;
for(int i=0;i<=up;i++)(ret+=dfs(p-1,(s+i)%n,limit&&i==up))%=mod;
if(!limit)f[p][s]=ret;
return ret;
}
ll solve(string s){
int tot=0;
for(int i=(int)s.size()-1;i;i--)num[++tot]=s[i]-'0';
memset(f,-1,sizeof(f));
return dfs(tot,0,1);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>s>>n;s=' '+s;
cout<<(solve(s)-1+mod)%mod<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing
T - Permutation
字首和最佳化 DP。
開始上強度了。
設 \(f_{i,j}\) 為填前 \(i\) 個數,第 \(i\) 個數為 \(j\) ,且前 \(i\) 個數為 \(1\) 到 \(i\) 的排列的方案數,每次加入新的數 \(j\) 時,可以將原來排列中所有大於等於 \(j\) 的數加一以保證仍是排列。不難發現,這樣依然滿足題目要求的性質。
由此,有轉移
不難使用字首和最佳化轉移。
程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=3005;
int n;string s;
ll f[N][N],g[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>s;s=' '+s;
f[1][1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++)g[j]=(g[j-1]+f[i-1][j])%mod;
for(int j=1;j<=i;j++)
if(s[i-1]=='>')f[i][j]=(g[i-1]-g[j-1]+mod)%mod;
else f[i][j]=g[j-1];
}
ll ans=0;
for(int j=1;j<=n;j++)(ans+=f[n][j])%=mod;
cout<<ans<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/10
//When I wrote a piece of code, her and I knew what it was doing
U - Grouping
狀壓 DP。
設 \(f_S\) 為 \(S\) 中的兔子可得的最高得分,則有轉移
其中, \(g_S\) 為 \(S\) 中的兔子分為一組的得分。
程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=17;
int n,a[N][N];
ll f[1<<N],g[1<<N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>a[i][j];
for(int S=0;S<(1<<n);S++)
for(int i=0;i<n;i++){
if(!((S>>i)&1))continue;
for(int j=i+1;j<n;j++){
if(!((S>>j)&1))continue;
g[S]+=a[i][j];
}
}
for(int S=0;S<(1<<n);S++)
for(int T=S;T;T=S&(T-1))
f[S]=max(f[S],f[S-T]+g[T]);
cout<<f[(1<<n)-1]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/11
//When I wrote a piece of code, her and I knew what it was doing
V - Subtree
換根 DP。
設點 \(1\) 為根,\(f_{u}\) 為以 \(u\) 為根的子樹的方案數,則有轉移
考慮換根,設 \(g_{u}\) 為以 \(u\) 為根的子樹之外的方案數,\(fa\) 為點 \(u\) 的父親,則有轉移
最終 \(f_u g_u\) 即為點 \(u\) 的答案。
考慮如何快速計算 \(\prod_{\{v \mid v \in \operatorname{son}(fa) \wedge v \neq u\}}\) ,一個直接的想法是從 \(fa\) 的子樹中扣去 \(u\) 子樹的答案,但由於模數非質且可能有 \(0\),逆元不一定存在。
考慮維護每個點的所有兒子的 \(f_u+1\) 的字首積和字尾積,計算時用前一個兄弟的字首積和後一個兄弟的字尾積計算答案。
程式碼
#include<iostream>
#include<cstdio>
#include<vector>
#define int long long
using namespace std;
using ll=long long;
const int N=1e5+5;
int n,mod;
vector<int>gr[N];
ll f[N],g[N],h[N];
void dfs(int p,int fa){
f[p]=1;
for(auto to:gr[p]){
if(to==fa)continue;
dfs(to,p),(f[p]*=f[to]+1)%=mod;
}
ll res=1;
for(int i=0;i<(int)gr[p].size();i++){
int to=gr[p][i];if(to==fa)continue;
g[to]=res,(res*=f[to]+1)%=mod;
}
res=1;
for(int i=gr[p].size()-1;~i;i--){
int to=gr[p][i];if(to==fa)continue;
(g[to]*=res)%=mod,(res*=f[to]+1)%=mod;
}
}
void redfs(int p,int fa){
if(p!=1)h[p]=(h[fa]*g[p]+1)%mod;
for(auto to:gr[p])if(to!=fa)redfs(to,p);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>mod;
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
gr[u].push_back(v);gr[v].push_back(u);
}
dfs(1,0);
h[1]=1,redfs(1,0);
for(int i=1;i<=n;i++)cout<<h[i]*f[i]%mod<<'\n';
return 0;
}
//coder:Iictiw
//date:24/11/11
//When I wrote a piece of code, her and I knew what it was doing
W - Intervals
資料結構最佳化 DP。
設 \(f_{i,j}\) 為考慮前 \(i\) 個位置,最近的 \(1\) 在 \(j\) 的最大得分。
對於這類區間帶權的題,一種套路化的處理方法是在右端點處更新答案。考慮轉移,若在第 \(i\) 位放一個 \(1\),則有
若在第 \(i\) 位放一個 \(0\) ,則有
直接做是 \(O(n^2)\) 的。不難發現,對於 \(i\) 相同的 \(f_{i,j}\),\(\sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k\) 是相等的, \(f_i\) 相較於 \(f_{i-1}\) 只有 \(f_{i,i}\) 一個位置在去掉 \(\sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k\) 後不同,於是可以讓 \(f_i\) 繼承 \(f_{i-1}\) 的 \(1\) 至 \(i-1\) 項,然後統一處理 \(\sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k\) 對答案的影響,注意到第 \(k\) 個區間影響的 \(f_{i,j}\) 是滿足 $ j \in [l_k, r_k]$ 的一段 \(f_{i,j}\) ,於是可以用線段樹維護每個區間對答案的影響。
程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
using ll=long long;
const int N=2e5+5;
int n,m;
struct node{int l,r,x;}a[N];
#define mid ((l+r)>>1)
#define ls (p<<1)
#define rs (p<<1|1)
ll mx[N<<2],tag[N<<2];
inline void push_up(int p){mx[p]=max(mx[ls],mx[rs]);}
inline void make_tag(int p,ll x){mx[p]+=x,tag[p]+=x;}
inline void push_down(int p){
if(tag[p]){
make_tag(ls,tag[p]),make_tag(rs,tag[p]);
tag[p]=0;
}
}
void update(int p,int l,int r,int L,int R,ll x){
if(l>=L&&r<=R)return make_tag(p,x);
if(l>R||r<L)return;
push_down(p);
update(ls,l,mid,L,R,x),update(rs,mid+1,r,L,R,x);
push_up(p);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>a[i].l>>a[i].r>>a[i].x;
sort(a+1,a+1+m,[&](node x,node y){
return x.r<y.r;
});
for(int i=1,j=1;i<=n;i++){
update(1,1,n,i,i,max(mx[1],0ll));
while(j<=m&&a[j].r<=i)update(1,1,n,a[j].l,a[j].r,a[j].x),j++;
}
cout<<max(0ll,mx[1])<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/14
//When I wrote a piece of code, her and I knew what it was doing
X - Tower
貪心維護 DP 轉移順序。
Lemma: 必然存在一種最優策略,使得若第 \(i\) 塊在第 \(j\) 塊下方,則有 \(s_i + w_i \geq s_j + w_j\)。
證明:考慮鄰項交換,對於相鄰的兩個塊 \(i\) 與 \(j\),\(j\) 在 \(i\) 上時,\(i\) 的剩餘載量為 \(s_i - w_j\);\(i\) 在 \(j\) 上時,\(j\) 的剩餘載量為 \(s_j - w_i\)。若 \(j\) 在 \(i\) 上不劣於 \(i\) 在 \(j\) 上,則必然有 \(s_i - w_j \geq s_j -w_i\) 。移項可得結論,進而容易推廣至任意 \(i,j\)。
因此,可以將所有塊以 \(s_i + w_i\) 為關鍵字降序排序,然後考慮 DP。
設 \(f_{i,j}\) 為放了前 \(i\) 塊,還能承載重量為 \(j\) 的最大價值,則若不放第 \(i\) 塊,有轉移
若放第 \(i\) 塊在其他塊上,則有轉移
若第 \(i\) 塊作為最底部的塊,則有轉移
程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
using ll=long long;
const int N=1e4+5;
int n,m;
struct node{int w,s,v;}a[N];
ll f[2][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i].w>>a[i].s>>a[i].v,m=max(m,a[i].s);
sort(a+1,a+1+n,[&](node x,node y){
return x.w+x.s>y.w+y.s;
});
int cur=0;
f[0][a[1].s]=a[1].v;
for(int i=2;i<=n;i++){
cur^=1;
for(int j=0;j<=m;j++)f[cur][j]=f[cur^1][j];
f[cur][a[i].s]=max(f[cur][a[i].s],(ll)a[i].v);
for(int j=0;j<=m;j++)
if(a[i].w<=j)
f[cur][min(a[i].s,j-a[i].w)]=max(f[cur][min(a[i].s,j-a[i].w)],f[cur^1][j]+a[i].v);
}
ll ans=0;
for(int i=0;i<=m;i++)ans=max(ans,f[cur][i]);
cout<<ans<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/14
//When I wrote a piece of code, her and I knew what it was doing
Y - Grid 2
容斥 DP。
直接計算方案數較為困難,考慮容斥,用總方案數減去經過牆的方案數。
一個直接的想法是經過 \(0\) 個牆的方案數 \(-\) 經過 \(1\) 個牆的方案數 \(+\) 經過 \(2\) 個牆的方案數 \(-\) 經過 \(3\) 個牆的方案數……但這種方式難以在低時間複雜度內計算。
考慮將經過牆的方案數不重不漏地表示出來。設 \(f_{i}\) 為在到達第 \(i\) 個牆前不經過其他任何牆的方案數,則可以同樣用容斥的方法計算 \(f_i\),即用到達第 \(i\) 個牆的方案數減去從其他牆到第 \(i\) 個牆的方案數,因此,有轉移
特別地,我們不妨設第 \(n+1\) 個牆位於 \((h,w)\) ,則 \(f_{n+1}\) 即為答案。
程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
#define x first
#define y second
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=2e5+5;
int n,m,k;
pair<int,int>a[N];
ll fct[N],ifct[N];
inline ll qpow(ll a,int p){
ll ret=1;
while(p){
if(p&1)(ret*=a)%=mod;
(a*=a)%=mod,p>>=1;
}
return ret;
}
inline ll C(int n,int m){
if(n<0||m<0||n<m)return 0;
return fct[n]*ifct[m]%mod*ifct[n-m]%mod;
}
ll f[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
for(int i=fct[0]=1;i<N;i++)fct[i]=fct[i-1]*i%mod;
ifct[N-1]=qpow(fct[N-1],mod-2);
for(int i=N-2;~i;i--)ifct[i]=ifct[i+1]*(i+1)%mod;
cin>>n>>m>>k;
for(int i=1;i<=k;i++)cin>>a[i].x>>a[i].y;
a[++k]={n,m};
sort(a+1,a+1+k);
for(int i=1;i<=k;i++){
f[i]=C(a[i].x+a[i].y-2,a[i].x-1);
for(int j=1;j<i;j++)
if(a[j].y<=a[i].y)
f[i]=(f[i]-f[j]*C(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod+mod)%mod;
}
cout<<f[k]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/15
//When I wrote a piece of code, her and I knew what it was doing
Z - Frog 3
斜率最佳化 DP。
請自行腦補 BGM
設 \(f_i\) 為到點 \(i\) 的最小代價,\(O(n^2)\) 的轉移是容易的,即
不妨設此時有兩個決策點 \(j,k (j \lt k)\),我們稱對於點 \(i\) 來說決策點 \(j\) 優於 \(k\),當且僅當由 \(j\) 點轉移至 \(i\) 點的代價優於由 \(k\) 點轉移至 \(i\) 點的代價。若點 \(j\) 優於點 \(k\),則有
化簡得
注意到 \(f_j + {h_j}^2,f_k + {h_k}^2\) 分別只與 \(j,k\) 有關,設 \(g(i) = f_i + {h_i}^2\),則有
即
由於題目保證 h 單調遞增,因此 \(h_j - h_k \lt 0\),於是最終得到
如果我們將 \(h_i\) 視為點 \(i\) 的橫座標, \(g(i)\) 視為點 \(i\) 的縱座標,這個式子就如同求點 \(j\) 與點 \(k\) 的連線的斜率。因此,這種最佳化方法稱為斜率最佳化。
類似與上式可得,若點 \(j\) 劣於點 \(k\),則有
若點 \(j\) 與點 \(k\) 不相上下,則有
我們不妨再設有 \(j_1,j_2,j_3 (j_1 \lt j_2 \lt j_3 \lt i)\) 三個決策點,思考若
意味著什麼。
分類討論,設 $slope(i,j) = \dfrac{g(i) - g(j)}{h_{i} - h_{j}} $ 以簡化式子。
-
當 \(2h_i \lt slope(j_2,j_3) \lt slope(j_1,j_2)\) 時:\(j_1\) 優於 \(j_2\);
-
當 \(slope(j_2,j_3) \leq 2h_i \leq slope(j_1,j_2)\) 時:\(j_1\) 不劣於 \(j_2\) , \(j_3\) 不劣於 \(j_2\) ;
-
當 \(slope(j_2,j_3) \lt slope(j_1,j_2) \lt 2h_i\) 時:\(j_3\) 優於 \(j_2\)。
不難發現,當 \(slope(j_2,j_3) \lt slope(j_1,j_2)\) 時, \(j_2\) 永遠不可能成為最優決策點。
在座標軸上,這表現為直線 \(j_1j_2\) 的斜率大於直線 \(j_2j_3\) 的斜率,此時 \(j_2\) 不可能成為最優決策點。
由此可得,所有可能的最優決策點滿足斜率單調遞增。
刪去所有不可能成為最優決策點的點,不難發現,剩下的點將會構成一個下凸殼:
因此,這種最佳化方法又稱為凸殼最佳化。
回到本題,考慮如何維護該下凸殼。不難發現本題中,決策點的橫座標(即 \(h_i\)) 單調遞增,因此每次只會在原凸殼的末尾加入一個點,可以用單調棧進行維護,以保證斜率單調遞增。
查詢時,即尋找斜率大於 \(2h_i\) 的最小的位於凸殼上的點,由於凸殼上斜率單調,可以用二分在凸殼上查詢出第一個斜率大於 \(2h_i\) 的點,做到 \(O(n\log n)\)。
但在本題中, \(h_i\) 單調遞增,因此斜率(即 \(2h_i\))單調遞增,於是最優決策點也單調遞增,滿足決策單調性。所以,我們可以用單調佇列維護最優決策點,每次從隊首彈出斜率不大於 \(2h_i\) 的點,從而找到最優決策點,時間複雜度為 \(O(n)\)。
結束了?並沒有。
從頭開始,回到原轉移方程 $ f_i = \min(f_j + {h_i}^2 - 2h_ih_j + {h_j}^2 + C) $ 。將僅與 \(i\) 有關的項和僅與 \(j\) 有關的項分離並去掉 \(min\),改寫為
設 \(f_j + {h_j}^2 = y, 2h_i = k, h_j = x, f_i - {h_i}^2 - C = b\),則轉移方程相當於一個一次函式表示式 \(y = kx + b\) 。此時,\((x,y)\) 相當於平面上一個點, \(k\) 相當於直線斜率,\(b\) 表示過點 \((x,y)\) 的斜率為 \(k\) 的直線的截距。注意到 \(k\) 和所有 \((x,y)\) 都是已知的,我們的任務就轉化為選擇一個點 \(j(j \lt i)\),使得過點 \((x_j,y_j)\) (即 \((h_j,f_j + {h_j}^2)\)) 的斜率為 \(k_i\)(即 \(2h_i\))直線截距最小。
不妨假設有一條斜率為 \(k_i\) 的直線自底向上平移,其碰到的第一個點 \((x_j,y_j)\) 即為最優決策點(因為此時截距最小)。
可以看出,所有可能的最優決策點必然位於下凸殼上,每次要找的點也就是凸殼上第一個斜率大於 \(k_i\) 的點。
維護下凸殼和尋找最優決策點,這與上一種做法殊途同歸。
程式碼(二分棧)
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
using ll=long long;
const int N=2e5+5;
int n;ll C,a[N];
ll f[N];
int st[N],top;
inline db g(int i){return f[i]+a[i]*a[i];}
inline db slope(int i,int j){return (g(i)-g(j))/(a[i]-a[j]);}
inline int calc(db k){
int l=1,r=top-1,ret=top;
while(l<=r){
int mid=(l+r)>>1;
if(slope(st[mid],st[mid+1])>=k)r=mid-1,ret=mid;
else l=mid+1;
}
return st[ret];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>C;
for(int i=1;i<=n;i++)cin>>a[i];
st[++top]=1;
for(int i=2;i<=n;i++){
int j=calc(2*a[i]);
f[i]=f[j]+(a[i]-a[j])*(a[i]-a[j])+C;
while(top>1&&slope(st[top-1],st[top])>=slope(st[top],i))top--;
st[++top]=i;
}
cout<<f[n]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/18
//When I wrote a piece of code, her and I knew what it was doing
程式碼(單調佇列)
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
using ll=long long;
const int N=2e5+5;
int n;ll C,a[N];
ll f[N];
int L=0,R=-1,Q[N];
inline db g(int i){return f[i]+a[i]*a[i];}
inline db slope(int i,int j){return (g(i)-g(j))/(a[i]-a[j]);}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>C;
for(int i=1;i<=n;i++)cin>>a[i];
Q[++R]=1;
for(int i=2;i<=n;i++){
while(L<R&&slope(Q[L],Q[L+1])<=2*a[i])L++;
f[i]=f[Q[L]]+(a[i]-a[Q[L]])*(a[i]-a[Q[L]])+C;
while(L<R&&slope(Q[R-1],Q[R])>=slope(Q[R],i))R--;
Q[++R]=i;
}
cout<<f[n]<<endl;
return 0;
}
//coder:Iictiw
//date:24/11/18
//When I wrote a piece of code, her and I knew what it was doing
後記
結束了?結束了。
結束了?沒結束。
結束了?結束了。
Fumo?fumo。