前言
( ext{CPS-S2020}) 已然临近,而 ( ext{DP}) 作为联赛中的常考内容,是必不可少的复习要点,现根据教练和个人刷题,整理部分好题如下(其实基本上是直接搬……)。
CF515C Drazil and Factorial
题目大意
定义 (F(x)=sum_{i=0}^{10^i<=x} ((x/10^i) mod 10) !) 。
给定一个十进制数 (a),共由 (n) 个数字组成。要找到最大正数 (x) ,满足以下两个条件:
- (x) 不包含任何数字 (0) 和数字 (1) 。
- (F(x)=F(a)) 。
题解
感觉很高级,然后发现是个打表题:
#include<bits/stdc++.h>
using namespace std;
const int N=25;
int n,a[N];
vector<int> bag[N];
vector<int> res;
int main()
{
bag[2].push_back(2);
bag[3].push_back(3);
bag[4].push_back(2);
bag[4].push_back(2);
bag[4].push_back(3);
bag[5].push_back(5);
bag[6].push_back(3);
bag[6].push_back(5);
bag[7].push_back(7);
bag[8].push_back(2);
bag[8].push_back(2);
bag[8].push_back(2);
bag[8].push_back(7);
bag[9].push_back(2);
bag[9].push_back(3);
bag[9].push_back(3);
bag[9].push_back(7);
cin>>n;
for(int i=1;i<=n;++i)
{
scanf("%1d",&a[i]);
for(int j=0;j<(int)bag[a[i]].size();++j)
res.push_back(bag[a[i]][j]);
}
sort(res.begin(),res.end());
for(int i=(int)res.size()-1;i>=0;--i) printf("%d",res[i]);
printf("
");
return 0;
}
然后瞎搞一下就做完了。
CF840C On the Bench
题目大意
给定一个序列 (a(a_ile 10^9)) ,长度为 (n(nle 300)) 。
试求有多少 (1) 到 (n) 的排列 (p_i) ,满足对于任意的 (2le ile n) 有 (a_{p_{i-1}} imes a_{p_i}) 不为完全平方数,答案对 (10^9+7) 取模。
题解
要先进行一次很巧妙的简化,不难想到,如果我们把每一个数内部的平方因子都除去,最后是不会对答案造成影响的,此时如果要形成完全平方数,只能是两个相等的数相乘。于是我们就将问题转化为了有多少种排列,相邻的数不能相等。这是一个板子题(板子我都不会)。
然后有两种思考方向,排列组合或者是老老实实 ( ext{DP}) ,我们选择后者(前者暂时不会,会补上的)。
定义 (f_{i,j,k}) 表示对于第 (i) 个数,前面有 (j) 组相邻的数相同, (j) 组中又有 (k) 组等于 (a_i) 。那么考虑将 (a_i) 放入,有 (3) 种情况,我们不妨设在 (i) 之前,已经有 (same) 个与 (a_i) 相等的数放入其中了:
- 产生新的相邻的数,位置数为 (2same-k) 。
- 断开相邻的数,位置数为 (j-k) 。
- 什么都没发生,位置数为 (i-(2same-k)-(j-k)) 。
用上述方法转移即可。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=305;
const int MOD=1e9+7;
int n,a[N];
int f[N][N][N];
signed main()
{
cin>>n;
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
for(int i=1;i<=n;++i)
{
for(int j=2;j*j<=a[i];++j)
while(a[i]%(j*j)==0) a[i]/=j*j;
}
sort(a+1,a+1+n);
int tmp=1;f[1][0][0]=1;
for(int i=2;i<=n;++i)
{
if(a[i]!=a[i-1])
{
for(int j=0;j<i-1;++j)
{
for(int k=1;k<=min(j,tmp-1);++k)
f[i-1][j][0]+=f[i-1][j][k],f[i-1][j][k]=0,f[i-1][j][0]%=MOD;
}
tmp=0;
}
// printf("%d %d
",i,tmp);
for(int j=0;j<i;++j)
{
for(int k=0;k<=min(j,tmp);++k)
{
if(j&&k) f[i][j][k]+=f[i-1][j-1][k-1]*(tmp*2-k+1)%MOD,f[i][j][k]%=MOD;
f[i][j][k]+=f[i-1][j+1][k]*(j+1-k)%MOD,f[i][j][k]%=MOD;
f[i][j][k]+=f[i-1][j][k]*(i-(j-k)-(tmp*2-k))%MOD,f[i][j][k]%=MOD;
// printf("%lld %lld %lld %lld
",i,j,k,f[i][j][k]);
}
}
tmp++;
}
printf("%lld
",f[n][0][0]);
return 0;
}
P4766 [CERC2014]Outer space invaders
题目大意
你要消灭所有的外星人。对于每一个外星人,你必须在一段区间时间 $left [ l_i,r_i ight ] $ 内将其消灭,同时他距离你 (d_i) 。
你有一种武器,你可以在任意时间使用他,消耗 (r_i) 的能量消灭此时距离你为 (r_i) 以内的外星人,问消灭所有的外星人的最少代价。
题解
这里他很巧妙的转化了问题。不难想到,如果是要消灭所有的外星人,必定是需要消除最大的一个的。如果我们选择在一个时间 (t_i) 消灭了当前最远的外星人,那么整一个时间轴会被分成 (3) 部分,时间轴完全位于 (t_i) 左右两边的外星人,和跨过 (t_i) 的外星人,而又因为我们消灭的外星人是当前区间最大的,所以跨过的这一部分外星人就会被消灭。
这样左右两部分又变成了和一开始条件相当的区间,可以分治,直到当前区间内没有外星人了。
写法有递归递推两种,都可以的。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=305,MAXN=1e4+5;
int t,n;
struct Alien{int op,ed,dis;}a[N];
int uni[N<<1],mp[MAXN],len=0;
int f[N<<1][N<<1];
void solve()
{
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d%d%d",&a[i].op,&a[i].ed,&a[i].dis);
len=0,memset(mp,0,sizeof(mp));
for(int i=1;i<=n;++i) uni[++len]=a[i].op,uni[++len]=a[i].ed;
sort(uni+1,uni+1+len),len=unique(uni+1,uni+1+len)-uni-1;
for(int i=1;i<=len;++i) mp[uni[i]]=i;
for(int i=1;i<=n;++i) a[i].op=mp[a[i].op],a[i].ed=mp[a[i].ed];
memset(f,0,sizeof(f));
for(int i=1;i<=len;++i)
{
for(int l=1,r=i;r<=len;++l,++r)
{
int now=-1;
for(int j=1;j<=n;++j)
if(l<=a[j].op&&a[j].ed<=r&&(now==-1||a[now].dis<a[j].dis)) now=j;
if(now==-1) continue;
f[l][r]=1e9+7;
for(int k=a[now].op;k<=a[now].ed;++k) f[l][r]=min(f[l][r],f[l][k-1]+f[k+1][r]+a[now].dis);
}
}
printf("%d
",f[1][len]);
return ;
}
int main()
{
cin>>t;
while(t--) solve();
return 0;
}
CF264C Choosing Balls
题目大意
你可以选若干个物品,若这次选择的物品与上次选的 (c_i) 相同那么这个的贡献就是 (a imes v_i) 否则是 (b imes v_i) 。要使得利益最大化。
题解
我们不难考虑到用 (f_i) 表示以颜色 (i) 为结尾的,到当前的最大利益。然后转移方程就很好写了。
然后又不难想到 (f_i) 是可以用线段树维护的,但是发现还是过不了。
仔细思考一下,发现 (f_i) 是单调递增的,所以我们只需要维护所有 (f_i) 的最大值和次大值就可以了。算是个套路。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
inline void cmax(int &a,int b){if(a<b)a=b;}
inline void cmin(int &a,int b){if(a>b)a=b;}
int n,m;
struct Ball{int col,val;}a[N];
int f[N];
int x,y,maxn1,maxn2;
signed main()
{
cin>>n>>m;
// printf("
--------------
");
for(int i=1;i<=n;++i) scanf("%lld",&a[i].val);
for(int i=1;i<=n;++i) scanf("%lld",&a[i].col);
while(m--)
{
scanf("%lld%lld",&x,&y);
for(int i=1;i<=n;++i) f[i]=-1e18-7;
maxn1=a[1].col,maxn2=0;
f[a[1].col]=a[1].val*y;
for(int i=2;i<=n;++i)
{
if(maxn1==a[i].col) cmax(f[maxn1],max(f[maxn1]+a[i].val*x,f[maxn2]+a[i].val*y));
else cmax(f[a[i].col],max(f[a[i].col]+a[i].val*x,f[maxn1]+a[i].val*y));
cmax(f[a[i].col],a[i].val*y);
if(a[i].col==maxn1) continue;
if(f[a[i].col]>=f[maxn1]) maxn2=maxn1,maxn1=a[i].col;
else if(f[a[i].col]>f[maxn2]) maxn2=a[i].col;
// printf("%lld %lld
",f[maxn1],f[maxn2]);
}
printf("%lld
",max(0ll,f[maxn1]));
}
}
AT2000 [AGC002F] Leftmost Ball
也是一道套路好题。
题目大意
给你 (n) 种颜色的球,每个球有 (k) 个,把这 (n imes k) 个球排成一排,把每一种颜色的最左边出现的球涂成白色(初始球不包含白色),求有多少种不同的颜色序列,答案对 (10^9+7) 取模。
题解
因为最后回答的是颜色序列,所以每个相同颜色的小球相同。我们不妨思考最后的合法序列有怎样的特征。
枚举一下可以发现,对于最终序列的任何一个位置,其前缀的白球数量一定不比其前缀的颜色种类(除白色)数少。所以我们不妨设 (f_{i,j}) 表示到第 (i) 个白球,且已经放完了 (j) 种颜色的方案数。
因为我们需要满足不重不漏,所以我们不妨规定每一次放入白球和每一种颜色第一个球的时候,必定放在当前的第一个空位,可以证明这样的操作可以包括所有答案同时不遗漏答案。由此,状态转移方程易得为:
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define Lint long long
const int N=2e3+5,K=2e3+5;
const Lint MOD=1e9+7;
int n,k;
Lint fac[N*K];
Lint ksm(Lint x,int k){Lint res=1;for(;k;k>>=1,x=x*x%MOD)if(k&1)res=res*x%MOD;return res;}
Lint cal(int n,int m){return fac[n]*ksm(fac[m],MOD-2)%MOD*ksm(fac[n-m],MOD-2)%MOD;}
Lint f[N][N];
int main()
{
cin>>n>>k;
if(k==1){printf("1
");return 0;}
fac[0]=1;
for(int i=1;i<=n*k;++i) fac[i]=fac[i-1]*i%MOD;
f[0][0]=1;
for(int i=1;i<=n;++i)
{
for(int j=0;j<=i;++j)
{
f[i][j]=f[i-1][j];
if(j) f[i][j]+=f[i][j-1]*(n-j+1)%MOD*cal(n*k-i-(j-1)*(k-1)-1,k-2)%MOD,f[i][j]%=MOD;
}
}
printf("%lld
",f[n][n]);
return 0;
}
AT4513 [AGC030D] Inversion Sum
超级有意思的转换,感觉自己根本想不到啊。
题目大意
给你一个长度为 (n) 的数列,然后给你 (q) 个操作,你可以选择操作或者不操作,问所有情况下逆序对的总和。
题解
一眼感觉毫无思路。然后考虑移动之后的逆序对个数的变化贡献,发现难以维护两者的大小关系,但是发现我们可以计算概率,即 (i) 比 (j) 大的概率。
因为概率乘上总次数,就是每一个情况的次数,我们对于两个位置 (i) 和 (j) ,我们只需要知道最后有多少种情况是 (i) 比 (j) 大的,所以我们不妨设 (f_{i,j}) 表示当前(可能已经进行了几次交换) (a_i) 比 (a_j) 大的概率,然后对于每一种交换,我们只需要找到与其相关的几个 (f_{i,j}) 进行更新就可以了,复杂度 (O(n(n+q))) ,可以接受。
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define Lint long long
const int N=3e3+5,M=3e3+5;
const Lint MOD=1e9+7;
int n,m,a[N];
struct Opt{int x,y;}b[M];
Lint ksm(Lint x,int k){Lint res=1;for(;k;k>>=1,x=x*x%MOD)if(k&1)res=res*x%MOD;return res;}
Lint f[N][N];
Lint inv=ksm(2,MOD-2);
Lint All,res=0;
int main()
{
cin>>n>>m,All=ksm(2ll,m);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
for(int i=1;i<=m;++i) scanf("%d%d",&b[i].x,&b[i].y);
for(int i=1;i<=n;++i)
{
for(int j=1;j<=n;++j)
f[i][j]=(a[i]>a[j]);
}
for(int i=1;i<=m;++i)
{
for(int j=1;j<=n;++j)
{
if(b[i].x==j||b[i].y==j) continue;
Lint tmp1=f[b[i].x][j],tmp2=f[b[i].y][j];
f[b[i].x][j]=(tmp1+tmp2)*inv%MOD;
f[b[i].y][j]=(tmp1+tmp2)*inv%MOD;
}
for(int j=1;j<=n;++j)
{
if(b[i].x==j||b[i].y==j) continue;
Lint tmp1=f[j][b[i].x],tmp2=f[j][b[i].y];
f[j][b[i].x]=(tmp1+tmp2)*inv%MOD;
f[j][b[i].y]=(tmp1+tmp2)*inv%MOD;
}
Lint tmp1=f[b[i].x][b[i].y],tmp2=f[b[i].y][b[i].x];
f[b[i].x][b[i].y]=f[b[i].y][b[i].x]=(tmp1+tmp2)*inv%MOD;
}
for(int i=1;i<=n;++i)
{
for(int j=i+1;j<=n;++j)
res+=f[i][j]*All%MOD,res%=MOD;
}
printf("%lld
",res);
}