校内教练分配了DP专题,感觉这里的题不错。
当时没有一题是在讲评之前想出来的(没救了)。
后来看了下其实没有很难(指大部分题),当时可能太困了没认真想。
总之还是菜吧。
T1
AGC033D.Complexity
如果直接DP设
f
[
i
]
[
j
]
[
k
]
[
l
]
f[i][j][k][l]
f[i][j][k][l]记录矩形,瓶颈在于状态数上。
考虑减少状态数,其实也是一个比较套路的方法,发现答是
l
o
g
log
log级别的,于是把其中一维状态改为答案,DP值改为那维状态,转移时考虑横着还是竖着切,一边直接用上一个转,一边利用单调性转即可,效率
O
(
n
3
l
o
g
n
)
O(n^{3}logn)
O(n3logn)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=205;
int n,m,f[2][N][N][N],a[N][N];
bool check(int j,int k,int l,int r){
int x=(k-j+1)*(r-l+1),y=a[k][r]-a[j-1][r]-a[k][l-1]+a[j-1][l-1];
return (!y || x==y);
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>m;
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){
char c; scanf(" %c",&c);
a[i][j]=(c=='#')+a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
for(int x=0;x<2;x++) for(int j=1;j<=n+1;j++)
for(int l=1;l<=m;l++) f[x][j][j-1][l]=N;
for(int j=1;j<=n;j++) for(int k=j;k<=n;k++){
for(int l=m;l;l--){
if(l==m) f[0][j][k][l]=m;
else f[0][j][k][l]=f[0][j][k][l+1];
while(!check(j,k,l,f[0][j][k][l])) f[0][j][k][l]--;
//cout<<j<<" "<<k<<" "<<l<<" "<<f[0][j][k][l]<<endl;
}
}
//return 0;
for(int i=1;;i++){
int x=(i&1);
if(f[!x][1][n][1]==m){
cout<<i-1<<endl; return 0;
}
for(int j=1;j<=n;j++) for(int k=j;k<=n;k++)
for(int l=1;l<=m;l++) f[x][j][k][l]=f[!x][j][k][f[!x][j][k][l]+1];
for(int j=1;j<=n;j++) for(int l=1;l<=m;l++){
for(int k=j,p=j;k<=n;k++){
while(p<k && min(f[!x][j][p][l],f[!x][p+1][k][l])<=min(f[!x][j][p+1][l],f[!x][p+2][k][l])) p++;
f[x][j][k][l]=max(f[x][j][k][l],min(f[!x][j][p][l],f[!x][p+1][k][l]));
}
}
}
return 0;
}
T2
CF585F.Digits of Number Pi
如果子串必须是
S
S
S的前缀,那么是一个经典的单个字符串匹配问题,用KMP+数位DP记录目前匹配到的位置即可。
而任意子串就相当于
S
S
S的所有后缀的前缀,是多个字符串匹配问题,又注意到有用的只有每个后缀的前
d
/
2
d/2
d/2个字符,对它们建立
A
C
AC
AC自动机即可,效率
O
(
10
d
2
s
)
O(10d^{2}s)
O(10d2s)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P=1e9+7;
const int N=55,M=50005,K=10;
char ch[M];
void read(int* a,int& n){
scanf("%s",ch+1); n=strlen(ch+1);
for(int i=1;i<=n;i++) a[i]=ch[i]-'0';
}
int n,m,a[M],to[M][K],fa[M],ct=1;
bool is[M];
void add(int l,int r){
int u=1;
for(int i=l;i<=r;i++){
int x=a[i];
if(!to[u][x]) to[u][x]=++ct;
u=to[u][x];
}
is[u]=1;
}
void bfs(){
queue<int>q; fa[1]=1;
for(int i=0;i<K;i++){
int& u=to[1][i];
if(u) fa[u]=1,q.push(u);
else u=1;
}
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=0;i<K;i++){
int& v=to[u][i];
if(v) fa[v]=to[fa[u]][i],q.push(v);
else v=to[fa[u]][i];
}
}
}
ll f[N][M];
ll cnt(int* s){
memset(f,0,sizeof(f));
int u=1;
for(int i=0;i<m;i++){
for(int k=0;k<s[i+1];k++){
int v=to[u][k]; if(is[v]) v=0;
(f[i+1][v]+=1)%=P;
}
for(int j=0;j<=ct;j++){
for(int k=0;k<K;k++) if(f[i][j]){
int v=to[j][k]; if(is[v]) v=0;
(f[i+1][v]+=f[i][j])%=P;
}
}
u=to[u][s[i+1]]; if(is[u]) u=0;
}
//cout<<(f[m][0]+(u==0))%P<<endl;
return (f[m][0]+(u==0))%P;
}
int x[N],y[N];
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
read(a,n),read(x,m),read(y,m);
for(int i=m;i;i--){
if(!x[i]) x[i]=9;
else{
x[i]--; break;
}
}
for(int i=1;i+m/2-1<=n;i++) add(i,i+m/2-1);
bfs(); //return 0;
/*for(int i=1;i<=ct;i++){
cout<<"u="<<i<<" fa="<<fa[i]<<endl;
for(int j=0;j<K;j++) cout<<to[i][j]<<" ";
puts("");
}*/
cout<<(cnt(y)+P-cnt(x))%P<<endl;
return 0;
}
T3
AGC030D.Inversion Sum
直接算每对
i
,
j
i,j
i,j的贡献,发现每次操作只会对
4
n
4n
4n对的值有影响,其他都是
∗
2
*2
∗2,于是把方案数改为期望即可,效率
O
(
n
2
+
n
q
)
O(n^{2}+nq)
O(n2+nq)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P=1e9+7,V=(P+1)>>1;
const int N=3005;
int n,q,a[N]; ll f[N][N],g[N][N],ans,s=1;
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>q;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){
f[i][j]=(a[i]<a[j]); g[i][j]=(a[i]>a[j]);
}
while(q--){
int x,y; scanf("%d%d",&x,&y);
f[x][y]=f[y][x]=(f[x][y]+f[y][x])*V%P;
g[x][y]=g[y][x]=(g[x][y]+g[y][x])*V%P;
for(int i=1;i<=n;i++) if(i!=x && i!=y){
f[x][i]=f[y][i]=(f[x][i]+f[y][i])*V%P;
g[x][i]=g[y][i]=(g[x][i]+g[y][i])*V%P;
f[i][x]=f[i][y]=(f[i][x]+f[i][y])*V%P;
g[i][x]=g[i][y]=(g[i][x]+g[i][y])*V%P;
}
(s*=2)%=P;
}
for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) (ans+=g[i][j])%=P;
cout<<ans*s%P<<endl;
return 0;
}
T4
卡常垃圾!!!
CF568E. Longest Increasing Subsequence
如果没有空位,那么是一个经典的求LIS的问题,直接用树状数组维护dp[i]表示以数字i为结尾的LIS即可。
注意到比较不好做的地方在于m个数每个只能用一次,如果多记一维目前用到哪效率会炸。但既然要求严格上升,那么重复用一个数肯定没有用,所以完全可以忽略这个限制。于是遇到一个空位,直接用
O
(
m
)
O(m)
O(m)对目前所有dp值进行更新即可(树状数组因为是前缀max可做到
O
(
m
)
O(m)
O(m)重构)。
记录方案时,如果要记录每个状态的转移点空间会炸,考虑先忽略空位,然后在LIS的相邻两个数之间,尽量按顺序填入这些空位,其它空位乱填即可,效率
O
(
m
k
+
n
l
o
g
n
)
O(mk+nlogn)
O(mk+nlogn)。
代码:(毒瘤题卡常卡到怀疑人生)
#include<bits/stdc++.h>
using namespace std;
int R(){
int s=0; bool p=0; char c=getchar();
while(!isdigit(c)){
if(c=='-') p=1; c=getchar();
}
while(isdigit(c)) s=(s<<3)+(s<<1)+(c-48),c=getchar();
if(p) s=-s; return s;
}
const int N=2e5+5;
int n,m,a[N],b[N],c[N],q[N],t;
#define st first
#define nd second
#define mp make_pair
pair<int,int> dp[N],mx[N],pt[N],tp[N];
void upd(pair<int,int> u,int x){
for(;x<=t;x+=(x&(-x))) if(u.st>mx[x].st) mx[x]=u;
}
pair<int,int> qry(int x){
pair<int,int> u=mp(0,0);
for(;x;x-=(x&(-x))) if(mx[x].st>u.st) u=mx[x];
return u;
}
int ps[N],tt;
int main()
{
cin>>n;
for(int i=1;i<=n;i++){
a[i]=R();
if(~a[i]) q[++t]=a[i];
}
cin>>m;
for(int i=1;i<=m;i++) b[i]=R(),q[++t]=b[i];
sort(q+1,q+t+1); t=unique(q+1,q+t+1)-q-1;
for(int i=1;i<=m;i++) b[i]=lower_bound(q+1,q+t+1,b[i])-q,c[b[i]]++;
sort(b+1,b+m+1); m=unique(b+1,b+m+1)-b-1;
for(int i=1;i<=n;i++){
if(~a[i]){
a[i]=lower_bound(q+1,q+t+1,a[i])-q;
pair<int,int> u=qry(a[i]-1); pt[i]=u;
u.st++; u.nd=i;
if(u.st>dp[a[i]].st) dp[a[i]]=u,upd(u,a[i]);
}
else{
for(register int i=1;i<=t;i++){
tp[i]=dp[i];
if(tp[i-1].st>tp[i].st) tp[i]=tp[i-1];
}
for(register int i=1;i<=m;i++){
pair<int,int> v=tp[b[i]-1]; v.st++;
if(v.st>dp[b[i]].st) dp[b[i]]=v;
}
for(register int i=1;i<=t;i++){
mx[i]=dp[i];
if(mx[i-1].st>mx[i].st) mx[i]=mx[i-1];
}
}
}
pair<int,int> u=qry(t);
while(u.st){
ps[++tt]=u.nd;
u=pt[u.nd];
}
reverse(ps+1,ps+tt+1); ps[tt+1]=n+1; a[n+1]=t+1;
for(int i=0,j=1;i<=tt;i++){
for(int k=ps[i]+1;k<ps[i+1];k++) if(a[k]==-1){
while(j<=a[ps[i]]) j++;
while(!c[j] && j+1<a[ps[i+1]]) j++;
if(c[j] && j<a[ps[i+1]]){
a[k]=j; c[j]--; j++;
}
}
}
for(int i=1,j=1;i<=n;i++) if(a[i]==-1){
while(!c[j]) j++; a[i]=j; c[j]--;
}
for(int i=1;i<=n;i++) printf("%d ",q[a[i]]);
return 0;
}
CF体验极差,此题卡常极其恶心,我卡了一天自闭了。
改了个地方以为常数能除2,调了几百年发现假了,心态炸裂。
它到底做错了什么???难道有任何地方有可能优化吗??????
关键是,为什么,没有人,和我一样,TLE在12???
垃圾题真的毁我青春,我不管了。
我对CF有阴影了,Atcoder多好啊(目前没wa过 虽然只写了4题 )。
T5
CF724E. Goods transportation
首先容易建出最大流的图,但边数是
O
(
n
2
)
O(n^{2})
O(n2)级别的,且每对
i
<
j
i<j
i<j之间都有连边,难以用数据结构优化。考虑转化为等价的最小割问题,发现割边只有每个点与
s
,
t
s,t
s,t点相连的边以及
s
s
s集合到
t
t
t集合的边,可直接DP,设
f
[
i
]
[
j
]
f[i][j]
f[i][j]为前
i
i
i个点有
j
j
j个属于
s
s
s集合,转移显然。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=10005; const ll F=1e18;
int n; ll s[N],t[N],c,f[2][N],ans=F;
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>c;
for(int i=1;i<=n;i++) scanf("%lld",&s[i]);
for(int i=1;i<=n;i++) scanf("%lld",&t[i]);
for(int i=0;i<2;i++) for(int j=0;j<=n;j++) f[i][j]=F;
f[0][0]=0;
for(int i=1;i<=n;i++){
int x=(i&1);
for(int j=0;j<=i;j++){
f[x][j]=f[!x][j]+s[i]+c*j;
if(j) f[x][j]=min(f[x][j],f[!x][j-1]+t[i]);
if(i==n) ans=min(ans,f[x][j]);
}
}
cout<<ans<<endl;
return 0;
}
T6
CF536D. Tavas in Kansas
对于博弈问题考虑倒着DP,首先把
s
,
t
s,t
s,t到每个点的最短路求出来离散化一下,设
f
[
i
]
[
j
]
[
0
/
1
]
f[i][j][0/1]
f[i][j][0/1]为
s
s
s取完距离它前
i
i
i近的点,
t
t
t取完距离它前
j
j
j近的点后,
s
/
t
s/t
s/t先手,之后对于
s
/
t
s/t
s/t的最优解,转移时枚举
s
/
t
s/t
s/t取到哪,显然可以前缀
m
i
n
min
min优化,效率做到
O
(
n
2
)
O(n^{2})
O(n2)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2005,M=2e5+5;
int n,m,S,T,a[N],hd[N],to[M],nx[M],wl[M],tt;
void add(int u,int v,int w){
nx[++tt]=hd[u]; to[hd[u]=tt]=v; wl[tt]=w;
}
ll d[2][N],F=1e15;
struct node{
int x; ll d;
bool operator < (const node& r) const{
return d>r.d;
};
};
priority_queue<node>q;
void bfs(int s,bool p){
for(int i=1;i<=n;i++) d[p][i]=F;
d[p][s]=0; q.push((node){s,0});
while(!q.empty()){
node u=q.top(); q.pop();
if(d[p][u.x]!=u.d) continue;
for(int e=hd[u.x];e;e=nx[e]){
int v=to[e]; if(u.d+wl[e]>=d[p][v]) continue;
d[p][v]=u.d+wl[e]; q.push((node){v,d[p][v]});
}
}
ll q[N]; memcpy(q,d[p],sizeof(d[p])); sort(q+1,q+n+1);
//cout<<"p="<<p<<endl;
for(int i=1;i<=n;i++) d[p][i]=lower_bound(q+1,q+n+1,d[p][i])-q;//cout<<d[p][i]<<" "; puts("");
}
int ms[N][N],mt[N][N];
ll ss[N][N],st[N][N],f[N][N],g[N][N],mf[N][N],mg[N][N];
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>m>>S>>T;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int u,v,w,i=1;i<=m;i++)
scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w);
bfs(S,0); bfs(T,1);
for(int i=0;i<=n;i++){
for(int j=1;j<=n;j++){
if(d[0][j]>i) st[i][d[1][j]]+=a[j],mt[i][d[1][j]-1]=d[1][j];
if(d[1][j]>i) ss[d[0][j]][i]+=a[j],ms[d[0][j]-1][i]=d[0][j];
}
for(int j=1;j<=n;j++) st[i][j]+=st[i][j-1],ss[j][i]+=ss[j-1][i];
for(int j=n;~j;j--){
if(!mt[i][j]) mt[i][j]=mt[i][j+1];
if(!ms[j][i]) ms[j][i]=ms[j+1][i];
}
/*cout<<"i="<<i<<endl;
for(int j=1;j<=n;j++) cout<<mt[i][j]<<" "; cout<<endl;
for(int j=1;j<=n;j++) cout<<st[i][j]<<" "; cout<<endl;
for(int j=1;j<=n;j++) cout<<ms[j][i]<<" "; cout<<endl;
for(int j=1;j<=n;j++) cout<<ss[j][i]<<" "; cout<<endl;*/
}
//cout<<ms[4][0]<<endl;
for(int i=n;~i;i--) for(int j=n;~j;j--){
if(ms[i][j]) f[i][j]=-ss[i][j]-mg[ms[i][j]][j];
mf[i][j]=f[i][j]-st[i][j];
if(j<n) mf[i][j]=min(mf[i][j],mf[i][j+1]);
if(mt[i][j]) g[i][j]=-st[i][j]-mf[i][mt[i][j]];
mg[i][j]=g[i][j]-ss[i][j];
if(i<n) mg[i][j]=min(mg[i][j],mg[i+1][j]);
//printf("i=%d j=%d f=%lld g=%lld\n",i,j,f[i][j],g[i][j]);
}
//if(n==468) cout<<f[0][0]<<endl;
//cout<<f[0][0]<<endl;
if(f[0][0]<0) puts("Cry");
else if(!f[0][0]) puts("Flowers");
else puts("Break a heart");
return 0;
}
/*
4 3
1 4
-1 -1 -1 -1
1 2 1
2 3 1
3 4 1
*/
突然发现,很多DP题都是一开始有个大概思路,然后懒得列式子在那里干瞪眼,越想越不可做,后来列出式子一切迎刃而解。
T7
AGC022E - Median Replace
先考虑对于一个给定的字符串如何判断是否合法。从前往后考虑一个个字符加进来的情况,注意到一些奇妙的性质:1.若当前开头有2个1,则必合法;2.对于相连的01或10,可选择忽视(和任何一个数结合结果都是那个数)。
考虑把性质1作为目标,则01直接忽视,10先保留,这样当前序列一定是一些1后接着一些0,只需判断最后的序列是否不少于2个1,或只有1个1而没有0。
对于问号,考虑DP记录状态,1的状态只有三种(大于等于2都一样),连续的3个0显然缩为一个更优,于是0也只有三种状态,效率
O
(
n
)
O(n)
O(n)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5,P=1e9+7;
char a[N]; int n,f[N][3][3];
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
scanf("%s",a+1); n=strlen(a+1); f[0][0][0]=1;
for(int i=0;i<n;i++)
for(int j=0;j<3;j++) for(int k=0;k<3;k++) if(f[i][j][k]){
int v=f[i][j][k];
for(int x=0;x<2;x++) if(x==a[i+1]-'0' || a[i+1]=='?'){
if(j){
int y;
if(!x){
y=j+1; if(y==3) y=1;
}
else y=j-1;
(f[i+1][y][k]+=v)%=P;
}
else{
if(k==2) (f[i+1][0][2]+=v)%=P;
else{
if(!x) (f[i+1][1][k]+=v)%=P;
else (f[i+1][0][k+1]+=v)%=P;
}
}
}
}
int ans=f[n][0][1];
for(int i=0;i<3;i++) (ans+=f[n][i][2])%=P;
cout<<ans<<endl;
return 0;
}
T8
CF708E. Student’s Camp
容易想到暴力:
f
[
i
]
[
l
]
[
r
]
f[i][l][r]
f[i][l][r]为前i行四连通,最后一行剩下的区间为
[
l
,
r
]
[l,r]
[l,r]的概率,前缀和优化转移可以做到
O
(
n
3
)
O(n^{3})
O(n3)。
发现瓶颈在于状态数,考虑利用左右独立的性质来优化。i这维无法避免,想要减少状态只能把单个改为前缀和,记
f
[
i
]
[
l
]
f[i][l]
f[i][l]为左端点为
l
l
l的所有DP值,
g
[
i
]
[
r
]
g[i][r]
g[i][r]为右端点为
r
r
r的所有DP值,
f
,
g
f,g
f,g的转移推出式子后可用前缀和优化到
O
(
1
)
O(1)
O(1),于是效率优化到
O
(
n
2
)
O(n^{2})
O(n2)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P=1e9+7;
ll fpw(ll a,ll x){
ll s=1;
for(;x;x>>=1,(a*=a)%=P) if(x&1) (s*=a)%=P;
return s;
}
const int N=1505;
int n,m,k; ll x,y;
ll p[N],sp[N],f[N],g[N],cf[N],sf[N],cg[N],sg[N];
void sum(){
for(int i=1;i<=m;i++){
cf[i]=(cf[i-1]+f[i])%P,cg[i]=(cg[i-1]+g[i])%P;
sg[i]=(sg[i-1]+cg[i]*p[i])%P;
}
for(int i=m;~i;i--) sf[i]=(sf[i+1]+cf[i]*p[m-i])%P;
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>m>>x>>y>>k;
(x*=fpw(y,P-2))%=P;
p[0]=1; for(int i=1;i<=k;i++) (p[0]*=(P+1-x))%=P; sp[0]=p[0];
for(int i=1;i<=m;i++){
p[i]=p[i-1]*x%P*fpw(P+1-x,P-2)%P*fpw(i,P-2)%P*(k-i+1)%P;
if(x==1 && i==k) p[i]=1;
sp[i]=(sp[i-1]+p[i])%P; //cout<<p[i]<<" ";
}
f[1]=g[m]=1; sum();
for(int i=1;i<=n;i++){ //cout<<"i="<<i<<endl;
for(int l=1;l<=m;l++){
f[l]=p[l-1]*(sf[l]-cg[l-1]*sp[m-l]%P+P)%P;
//cout<<f[l]<<" ";
} //puts("");
for(int r=1;r<=m;r++){
g[r]=p[m-r]*(sp[r-1]*cf[r]%P-sg[r-1]+P)%P;
//cout<<g[r]<<" ";
} //puts("");
sum();
}
cout<<cf[m]<<endl;
return 0;
}
T9
ARC093F - Dark Horse
题目相当于有长度为
2
0
,
2
1
,
.
.
.
,
2
n
−
1
2^{0},2^{1},...,2^{n-1}
20,21,...,2n−1这
n
n
n个区间,要在上面填上
2
,
.
.
.
,
n
2,...,n
2,...,n,使得每个区间的最小值都不为给定的
m
m
m个数的方案数(最后再乘个
n
n
n)。
显然最小值不为某些数是不好计算的,考虑容斥。又注意到
m
m
m很小,枚举哪些人一定是区间最小的,按照编号从大到小考虑那些人。记
f
[
i
]
[
j
]
f[i][j]
f[i][j]为考虑了前
i
i
i个人,
n
n
n个区间分别是否已经有一定是最小值的人,只考虑这些区间的排列情况的方案数,转移时枚举这个人是否一定最小,以及如果是,这个人在哪个区间,乘上组合数即可。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=17,M=(1<<N);
const ll P=1e9+7;
ll fc[M],iv[M];
ll A(int n,int m){
if(m<0 || m>n) return 0;
return fc[n]*iv[n-m]%P;
}
ll fpw(ll a,ll x){
ll s=1;
for(;x;x>>=1,(a*=a)%=P) if(x&1) (s*=a)%=P;
return s;
}
int n,m,a[N],p[N];
ll f[N][M],ans;
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
fc[0]=1; for(int i=1;i<M;i++) fc[i]=fc[i-1]*i%P;
iv[M-1]=fpw(fc[M-1],P-2);
for(int i=M-2;~i;i--) iv[i]=iv[i+1]*(i+1)%P;
cin>>n>>m; p[0]=1; for(int i=1;i<=n;i++) p[i]=(p[i-1]<<1);
for(int i=1;i<=m;i++) scanf("%d",&a[i]);
reverse(a+1,a+m+1); f[0][0]=1;
for(int i=0;i<m;i++){
for(int j=0;j<p[n];j++) if(f[i][j]){
//printf("i=%d j=%d f=%lld\n",i,j,f[i][j]);
ll x=f[i][j]; int s=0;
for(int k=0;k<n;k++) if(j&p[k]) s+=p[k];
(f[i+1][j]+=x)%=P;
for(int k=0;k<n;k++) if(!(j&p[k])){
//cout<<k<<" "<<x*A(p[n]-a[i+1]-s,p[k]-1)%P*p[k]<<endl;
(f[i+1][j+p[k]]+=x*A(p[n]-a[i+1]-s,p[k]-1)%P*p[k])%=P;
}
}
}
for(int i=0;i<p[n];i++){
int s=0,t=1;
for(int j=0;j<n;j++){
if(!(i&p[j])) s+=p[j];
else t=-t;
}
(ans+=P+fc[s]*f[m][i]%P*t)%=P;
}
cout<<ans*p[n]%P<<endl;
return 0;
}
T10
ARC096F - Sweet Alchemy
注意到父子关系对个数的限制比较麻烦,直接树形DP记录不下。利用父子关系做一个转化:选一个点时把它的子树也选了,这样问题转化成
n
n
n个物品,每个物品有价值
v
v
v和重量
w
w
w,除了一号物品外每个物品不能选超过
D
D
D个,求在
X
X
X的容量内的最大选取价值总和。
这是个经典的多重背包问题,但只有
n
n
n和
v
v
v的范围是
50
50
50,必须利用贪心把物品数减少(物品数
1
e
9
1e9
1e9级别肯定做不了
D
P
DP
DP)。因为
v
v
v很小,从它出发考虑,使
v
v
v一定时,
w
w
w尽可能小。考虑一个物品选很多个时是否可被替换成更优:发现物品
i
i
i选了
v
[
j
]
v[j]
v[j]个时,能被
v
[
i
]
v[i]
v[i]个物品
j
j
j替换,只要
v
[
i
]
v[i]
v[i]个物品
j
j
j的重量更小就更优。于是按照
v
/
w
v/w
v/w从大到小排序,先考虑不会被替换的部分,只需记
f
[
i
]
f[i]
f[i]为价值为
i
i
i的最小重量,每个物品只需考虑
n
n
n个,剩下的再从前往后贪心即可。
DP状态是
O
(
n
3
)
O(n^{3})
O(n3)的,转移时对于每个物品,DP值按照
%
v
[
i
]
\% v[i]
%v[i]分组,每组间用单调队列维护最优转移点,一次转移可以做到
O
(
n
)
O(n)
O(n),总复杂度
O
(
n
4
)
O(n^{4})
O(n4)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=55,M=N*N*N;
int n,m,d,fa[N];
struct node{
int v; ll w;
}a[N];
bool cmp(node x,node y){
return y.w*x.v>x.w*y.v;
}
ll f[M],F=1e15;
int ans,p[M]; node q[M];
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>m>>d>>a[1].w; a[1].v=1;
for(int i=2;i<=n;i++) scanf("%lld%d",&a[i].w,&fa[i]),a[i].v=1;
for(int i=n;i>1;i--) a[fa[i]].w+=a[i].w,a[fa[i]].v+=a[i].v;
sort(a+1,a+n+1,cmp);
f[0]=0; for(int i=1;i<M;i++) f[i]=F;
int l=min(d,n),s=0;
for(int i=1;i<=n;i++){
s+=n*a[i].v;
for(int x=0;x<a[i].v;x++){
int t=0,ft=0,tl=0;
for(int j=x;j<=s;j+=a[i].v) p[++t]=j;
for(int j=t,k=t;j;j--){
while(ft<tl && q[ft+1].v>j) ft++;
while(k && k>=j-l){
node u=(node){k,f[p[k]]-a[i].w*k};
while(ft<tl && q[tl].w>=u.w) tl--;
q[++tl]=u;
k--;
}
f[p[j]]=min(f[p[j]],q[ft+1].w+a[i].w*j);
}
}
}
for(int i=0;i<=s;i++){
//printf("i=%d x=%lld\n",i,f[i]);
if(f[i]>m) continue;
int x=m-f[i],su=i;
for(int j=1;j<=n;j++){
int u=(int)(((ll)x)/a[j].w);
if(a[j].v<n) u=min(u,d-l);
su+=a[j].v*u; x-=a[j].w*u;
if(x<a[j].w) break;
}
//cout<<"su="<<su<<endl;
ans=max(ans,su);
}
cout<<ans<<endl;
return 0;
}
T11
CF559E. Gerald and Path
首先把所有出现的点离散化一下。
因为既要考虑区间的前后关系,又要使一个点的左右两个区间不能同时被选,故需要记录状态
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示前
i
i
i个最右覆盖到
j
j
j的最长覆盖长度。又因为这样在转移时限制较多比较麻烦,故把
j
j
j改为考虑到前
j
j
j个位置(后面不会在覆盖)而不一定覆盖。
设当前点为
(
l
,
p
,
r
)
(l,p,r)
(l,p,r),转移时,考虑这个点向左还是向右:向右就由
f
[
i
−
1
]
[
p
]
f[i-1][p]
f[i−1][p]转移到
f
[
i
]
[
r
]
f[i][r]
f[i][r];向左由于要考虑前面向右的区间可能会伸出去(超过
r
r
r),就枚举
k
k
k,使
k
+
1
,
.
.
,
i
−
1
k+1,..,i-1
k+1,..,i−1都向右,计算一下最右覆盖到哪里,由
f
[
k
]
[
l
]
f[k][l]
f[k][l]转移过来。该过程用前缀max
、后缀max
优化一下即可做到
O
(
n
2
)
O(n^{2})
O(n2)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=305;
struct node{
int l,p,r;
}a[N];
bool cmp(node x,node y){
return x.p<y.p;
}
int n,m,p[N],f[N][N],g[N];
void lb(int& x){
x=lower_bound(p+1,p+m+1,x)-p;
}
int main()
{
srand(time(0));
cin>>n;
for(int d,i=1;i<=n;i++){
scanf("%d%d",&a[i].p,&d);
a[i].l=a[i].p-d; a[i].r=a[i].p+d;
p[++m]=a[i].l; p[++m]=a[i].p; p[++m]=a[i].r;
}
sort(p+1,p+m+1); m=unique(p+1,p+m+1)-p-1;
for(int i=1;i<=n;i++) lb(a[i].l),lb(a[i].p),lb(a[i].r);
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
int l=a[i].l,x=a[i].p,r=a[i].r;
memcpy(f[i],f[i-1],sizeof(f[i-1]));
memset(g,0,sizeof(g));
int mx=x;
g[mx]=f[i-1][l]+p[mx]-p[l];
for(int j=i-1;j;j--){
mx=max(mx,a[j].r);
g[mx]=max(g[mx],f[j-1][l]+p[mx]-p[l]);
}
for(int j=m;j>=l;j--){
f[i][j]=max(f[i][j],g[j]);
g[j-1]=max(g[j-1],g[j]+p[j-1]-p[j]);
}
for(int j=x;j<=r;j++) f[i][j]=max(f[i][j],f[i-1][x]+p[j]-p[x]);
for(int j=1;j<=m;j++) f[i][j]=max(f[i][j],f[i][j-1]);
}
cout<<f[n][m]<<endl;
return 0;
}
T12
AGC022F - Checkers
最近做的最神仙的一道DP/计数题。
原本的想法是A关于B对称,就新建一个点C,A向这个点连一条-1的边,B连2的边,这样两种方案不同当且仅当存在一个点到根路径的乘积不同,但这样要同时关心2,-1的个数,难以计数。
发现乘积只跟-1的奇偶性有关,先考虑2的边,把-1的边缩起来,直接把A放到C上,这样就相当于B连向A,把A取反,于是问题变为计算这样的n个点的有标号的树的个数,两棵树不同当且仅当有一个点的深度或正负号不同。
发现正负号的问题比较麻烦,一个点会被所有儿子取反一次,然后被所有祖先的儿子取反一次。可以直接利用父亲的正负来推出儿子,但如果直接记录当前层有多少个正、负点的话,效率难以优化过
O
(
n
5
)
O(n^{5})
O(n5)。
但发现两颗仅存在正负号不同的点的树,将点与父亲做差分(即相同为1,不同为-1)后也不同,反之亦然。于是直接记录当前层差分为-1的点的个数,枚举下一层差分为-1,1的点数(不考虑下一层的儿子影响),可计算出下一层差分为-1的点数(考虑下一层的儿子影响,这里直接把所有的差距由1改为-1或-1改为1,因为同时把1改为-1且把1改为-1会被其它不同时改的方案记录到),效率为
O
(
n
4
)
O(n^{4})
O(n4)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P=1e9+7;
const int N=55;
int n; ll c[N][N],f[N][N];
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n; c[0][0]=1;
for(int i=1;i<=n;i++){
c[i][0]=1;
for(int j=1;j<=i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%P;
}
f[1][0]=f[1][1]=n;
for(int i=1;i<n;i++) for(int j=0;j<=i;j++){
//printf("i=%d j=%d f=%lld\n",i,j,f[i][j]);
for(int x=0;x<=n-i;x++) for(int y=0;x+y<=n-i;y++) if(x+y>=max(j,1) && !((x+y-j)&1)){
(f[i+x+y][abs(j+(x+y-j)/2-x)]+=f[i][j]*c[n-i][x+y]%P*c[x+y][x])%=P;
//cout<<x<<" "<<y<<" "<<abs(j+(x+y-j)/2-x)<<endl;
}
}
cout<<f[n][0]<<endl;
return 0;
}
感觉最近做题缺乏自信,很多本来自己有能力想出来的东西却去看了题解,这样对思维的锻炼是大打折扣的,还是要鼓励自己独立思考吧。