9月1-5日小记
9月1日
1. P1616 疯狂的采药
这显然是完全背包,但我们提出一种不同的理解。
完全背包之所以正序遍历,是因为每一个物品都可以无限取。
反观0-1背包,所遍历容量j的决策值dp[j]在转移过程中需要用到dp[j-a[i]]的值。显然j-a[i]严格小于j,所以在遍历时,dp[j-a[i]]的值在dp[j] 后更新,转移dp[j]时还没有更新。至于完全背包,dp[j-a[i]]的值在dp[j] 前更新。
考虑一个完全背包的非滚动数组转移方程:
而0-1背包的非滚动数组转移方程:
注意到max函数的第二项不同。所以,完全背包转移时,依赖当前行的前驱状态;0-1背包转移时,依赖上一行的前驱状态。而且,不管哪种背包,转移时不是依赖当前行的前驱状态,就是依赖上一行的前驱状态。这一思想极其重要。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1e4+5,M=1e7+5;
int t,m,a[N],b[N],f[M];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>t>>m;
for(int i=1;i<=m;i++){
cin>>a[i]>>b[i];
}
for(int i=1;i<=m;i++){
for(int j=a[i];j<=t;j++){
f[j]=max(f[j],f[j-a[i]]+b[i]);
}
}
cout<<f[t];
return 0;
}
2. P1450 [HAOI2008] 硬币购物
预备知识:容斥原理。
狭义的容斥原理一般用于计算多个集合的并的元素个数。推广而来,广义的容斥原理则用于计算满足多种性质的总元素个数。(可能表述的不太清)也可以把一个集合内的所有元素看作所有满足某个性质的元素。
容斥原理的一般形式是:
举例:当\(n=3\)时,有:
其中\(|A|\)表示集合\(A\)的元素个数。
这题可以先处理完全背包,然后用凑出金额s的不加限制的总方案数减去不合法的方案数。这就涉及到容斥原理。
定义f[j]。显然f[j]可以从所有的f[j-c[i]]转移而来,并累加求和,相当于f*1的乘法原理。
所以如果第i种硬币使用超限,那么一定用了大于等于(d[i]+1)个i种硬币,相当于状态从f[j-c[i]*(d[i]+1)]转移而来。
超限时,硬币数大于d[i]+1的情况则一定会被包含在f[j-c[i]*(d[i]+1)]里,所以我们只需要考虑f[j-c[i]*(d[i]+1)]即可。
但是我们想到,同时计算四种金币的不合法方案数可能会有算重的情况,于是我们考虑容斥原理(可以画Venn图来形象理解):不加限制的总方案数 - (一种金币超限的方案数 - 两种金币超限的方案数 + 三种金币超限的方案数 - 四种金币超限的方案数) = 答案(即奇加偶减)。
后面括号里的运算结果就是所有不合法的方案总数。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int M=1e5+10;
int c[5],d[5],f[M],n,s,ans;
int judge(int x){
int cnt=0;
while(x){
cnt++;
x-=x&(-x);
}
return (cnt%2==1)?-1:1;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
for(int i=1;i<=4;i++){
cin>>c[i];
}
cin>>n;
f[0]=1;
for(int i=1;i<=4;i++){
for(int j=c[i];j<=M-10;j++){
f[j]+=f[j-c[i]];
}
}
while(n--){
ans=0;
for(int i=1;i<=4;i++){
cin>>d[i];
}
cin>>s;
for(int i=0;i<16;i++){//枚举二进制状态
//使用四位二进制数来表示目前考虑第几个集合的交
int sum=0;
for(int j=0;j<4;j++){//枚举状态i的每一位
if(i & (1<<j)){
sum+=c[j+1]*(d[j+1]+1);
}
}
if(s-sum>=0)ans+=judge(i)*f[s-sum];
}
cout<<ans<<endl;
}
return 0;
}
9月2日
1. P2167 [SDOI2009] Bill的挑战
状压预处理+状压DP。好题。
首先观察数据范围,注意到N<=15,所以考虑压缩N及其相关量。
我们在枚举串T的每一位时,对于每一位的元素,这一元素对于所给出的n个字符串s[]在它所在的位置,只有能匹配或不能匹配两种选择,正好对应二进制中的两种状态。
例如,N=3,len=4(即字符串长度),下面这组数据:
? c ? b ? ?
a ? ? ? ? ?
? d ? ? e ?
T: _ _ _ _ _ _
假如现在枚举到串T的第二个元素,如果这一位填入字母d,那么对于s[1],它不匹配;对于s[2],它匹配;对于s[3],它匹配。
所以,我们可以开一个数组g[i][c],它表示:串T的第i个位置填入字母c-'a',对于所有给定的串s的匹配状态。比方说上面的例子,g[2]['d'-'a']=011。
显然,g数组可以预处理。下面开始转移。
考虑到串T的枚举位数和匹配状态都会影响目标答案,所以将它们融入状态定义:定义f[i][st]表示串T枚举到第i位,串T的匹配状态为st,此时串T可能的总情况数。
似乎不太好转移。这是一道求方案数的DP。让我们想想另一道求方案数的DP题:完全背包求方案数。
这个问题的转移方程为f[j]+=f[j-v[i]]。价值j显然大于价值(j-v[i]),即前者是后者的后继状态。所以我们发现,后继状态总由前驱状态转移而来(即动态规划问题的无后效性)。这个状态转移方程还能写成f[j+v[i]]+=f[j]。
类比本题,比方说现在枚举到串T的第i位,则这一位的状态必由第(i-1)位的状态转移而来。所以我们考虑状态f[i-1][j]被包含在了哪种状态里,即由被哪种状态转移。
不难想到f[i][j&g[i][k]]包含f[i-1][j]。因为在枚举字母k时,它的匹配状态g[i][k]必须和上一步的状态j按位与,才能得到现在的状态。即状态转移方程是f[i][j&g[i][k]]+=f[i-1][j]。
答案统计:所有二进制1的个数等于k的,长度等于字符串长度的f数组值的和。记得取模。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int mod=1000003;
int q,n,len,tlen,ans,g[55][30],f[55][1<<15];
string s[18];
int getcnt(int x){
int cnt=0;
while(x){
cnt++;
x-=x&(-x);
}
return cnt;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>q;
while(q--){
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
cin>>n>>tlen;
for(int i=1;i<=n;i++){
cin>>s[i];
s[i]=' '+s[i];
}
len=s[1].length()-1;
for(int i=1;i<=len;i++){//位数
for(int j=0;j<26;j++){//字母
for(int k=1;k<=n;k++){//行数
if(s[k][i]=='?' || s[k][i]==(j+'a')){
g[i][j] |= (1<<(k-1));
}
}
}
}
f[0][(1<<n)-1]=1;
for(int i=1;i<=len;i++){
for(int j=0;j<(1<<n);j++){
if(f[i-1][j]==0)continue;
for(int k=0;k<26;k++){
f[i][j&g[i][k]]=(f[i][j&g[i][k]]+f[i-1][j])%mod;
}
}
}
ans=0;
for(int j=0;j<(1<<n);j++){
if(getcnt(j)==tlen){
ans=(ans+f[len][j])%mod;
}
}
cout<<ans<<endl;
}
return 0;
}
2. P3800 Power收集
线性DP+单调队列优化。
状态定义不说了,状态转移方程也显然是f[i][j]=max(f[i-1][k])+a[i][j],其中j-t<=k<=j+t。
如果分别枚举i、j、k,时间复杂度会变为三次方级的,故考虑优化。
注意到我们在遍历每个j的时候,都会产生一个固定长度2t+1的窗口,且这个窗口的中心点是j。
也就是说,我们可以使用滑动窗口维护该窗口的最值,但是这个窗口是中心对称的,所以我们不太好用一遍的队列操作直接得到f[i-1][k]的最大值。
我们考虑操作两遍:从左转移来的、从右转移来的。
对于每一遍操作,相当于维护一个长度折半的滑动窗口,易于编码。
修改:对于每次操作的状态转移,不需要加任何判断条件。
如果加了判断条件(窗口是否成型),那么当窗口未成形时,将不会进行任何状态转移,经过计算的状态会少一些,会干扰以后的计算。
而不加判断条件的话,单调队列的队头也会不断返回使f[i-1][k]最大的k,当然可以用于状态转移。所以不需要加判断条件。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=4010;
int n,m,k,t,a[N][N],f[N][N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>k>>t;
while(k--){
int x,y,v;
cin>>x>>y>>v;
a[x][y]=v;
}
for(int j=1;j<=m;j++){
f[1][j]=a[1][j];
}
for(int i=2;i<=n;i++){
deque<int>dq;
//从左转移来的
for(int j=1;j<=m;j++){
while(!dq.empty()&&f[i-1][dq.back()]<=f[i-1][j]){
dq.pop_back();
}
while(!dq.empty()&&dq.front()<j-t){
dq.pop_front();
}
dq.push_back(j);
f[i][j]=f[i-1][dq.front()]+a[i][j];
}
dq.clear();
//从右转移来的
for(int j=m;j>=1;j--){
while(!dq.empty()&&f[i-1][dq.back()]<=f[i-1][j]){
dq.pop_back();
}
while(!dq.empty()&&dq.front()>j+t){
dq.pop_front();
}
dq.push_back(j);
f[i][j]=max(f[i][j],f[i-1][dq.front()]+a[i][j]);
}
}
int ans=0;
for(int j=1;j<=m;j++){
ans=max(ans,f[n][j]);
}
cout<<ans;
return 0;
}
9月3日
1. P3572 [POI 2014] PTA-Little Bird
线性DP+单调队列优化。
定义f[i]表示走到第i格所需的最小劳累值,可以考虑纯线性DP的写法:f[i]=f[i-p]+(a[i]>=a[i-p]),其中1<=p<=k。
注意到这样枚举i和p的时间复杂度达到二次方形式,故考虑优化。
显然,k是个定值(滑动窗口所维护的区间一般需定长),那么就可以想象:对于所枚举的每个i,都有一个长度为k的严格单调递减的滑动窗口在后面跟着,用于维护长度为定长的区间内,最小的f[i-p]。
需要特判的是,如果f[dq.back()](也就是目前认为最小的f[i-p])恰好与f[i]相等,那么需要进一步比较。
考虑一个贪心:再次维护这个队列,使得队列内的a[i-p]始终单调递增,进而使得队首的a[i-p]始终最大,使(a[i]>=a[i-p])尽可能为假。
注意滑动窗口代码模板实现的顺序:
- 移除过期元素
- 转移
- 维护队列单调性
- 入栈
这个顺序可以保证转移时所使用的队首元素始终不过期。不同滑动窗口题目中,代码模板的实现顺序也可能不同。(待议)
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1e6+10;
int n,a[N],q,k,f[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cin>>q;
while(q--){
deque<int>dq;
memset(f,0,sizeof(f));
dq.push_back(1);
cin>>k;
for(int i=2;i<=n;i++){
while(!dq.empty()&&dq.front()<i-k){
dq.pop_front();
}
f[i]=f[dq.front()]+(a[i]>=a[dq.front()]);
while(!dq.empty()&&((f[dq.back()]>f[i]) || (f[dq.back()]==f[i] && a[dq.back()]<=a[i]))){
dq.pop_back();
}
dq.push_back(i);
}
cout<<f[n]<<endl;
}
return 0;
}
2. P7567 「MCOI-05」魔仙
神秘构造,待补全。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int T,n;
signed main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);
if(n%4==0){
int k=n/4;
if(k%2){
printf("2 -%d ",k*2);
for(int i=1;i<=3*k-2;i++){
printf("1 ");
}
for(int i=1;i<=k;i++){
printf("-1 ");
}
}else{
printf("-2 -%d ",k*2);
for(int i=1;i<=3*k;i++){
printf("1 ");
}
for(int i=1;i<=k-2;i++){
printf("-1 ");
}
}
printf("\n");
}else{
printf("w33zAKIOI\n");
}
}
return 0;
}
3. P6394 樱花,还有你
线性DP+前缀和优化。
设f[i][j]表示走到第i格,恰好收集到j朵樱花的方案数。
显然f[i][j]+=f[i-1][j-k],其中1<=k<=a[i]。
注意到两个事情:
i在转移过程中没用,可以滚掉;- 所累加的
f[j-k]中的k是连续的,所以累加的值也是连续的,可以用前缀和优化。
开一个前缀和数组s[j]表示f[1]到f[j]的和。
状态转移方程显然为f[j]=s[j]-s[j-a[i]-1],当且仅当j>a[i];如果不,则f[j]=s[j]。
每次循环枚举i的时候,都要将f[n]累加到ans中,并重新预处理前缀和数组。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=5e3+10,mod=10086001;
int n,k,a[N],f[N],s[N],tot,ans;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>k;
for(int i=1;i<=k;i++){
cin>>a[i];
tot+=a[i];
}
if(tot<n){
cout<<"impossible";
return 0;
}
f[0]=s[0]=1;
for(int i=1;i<=k;i++){
s[0]=f[0];
for(int j=1;j<=n;j++){
s[j]=(s[j-1]+f[j])%mod;
}
for(int j=0;j<=n;j++){
if(j>a[i]){
f[j]=(s[j]-s[j-a[i]-1])%mod;
}else{
f[j]=s[j];
}
}
ans=(ans+f[n])%mod;
}
cout<<(ans+mod)%mod;
return 0;
}
4. P11248 [GESP202409 七级] 矩阵移动
这题显然不是橙。
三维DP。形象地说,二维的矩阵影响决策,一维的操作数也影响决策。
定义f[i][j][k]表示当前走到点(i,j),已修改k个'?'的最大分数。
显然,不管a[i][j]上的元素是什么,都有:f[i][j][k]=max(f[i-1][j][k],f[i][j-1][k])+(a[i][j]=='1');
特别地,如果a[i][j]上的元素是'?',且目前所枚举的k>0,则:f[i][j][k]=max(f[i][j][k],max(f[i-1][j][k-1],f[i][j-1][k-1])+1)。
答案为f[n][m][k]的最大值。
提交,MLE所有点,考虑滚动数组优化。
注意到每一行的所有状态均由这一行和这一行的前一行转移而来,所以可以考虑滚掉第一维。
将所有的i-1换为(i+1)&1,将所有的i换为i&1即可。这里利用了本文最开始介绍的思想。(i+1)&1表示从上一行转移而来,i&1则表示从这一行转移而来。
最后答案即为f[n&1][m][k]的最大值。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=505,M=305;
int q,n,m,t,ans,f[2][N][M];
char a;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>q;
while(q--){
memset(f,0,sizeof(f));
cin>>n>>m>>t;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a;
for(int k=0;k<=t;k++){
f[i&1][j][k]=max(f[(i+1)&1][j][k],f[i&1][j-1][k])+(a=='1');
if(a=='?' && k>0){
f[i&1][j][k]=max(f[i&1][j][k],max(f[(i+1)&1][j][k-1],f[i&1][j-1][k-1])+1);
}
}
}
}
ans=0;
for(int k=0;k<=t;k++){
ans=max(ans,f[n&1][m][k]);
}
cout<<ans<<endl;
}
return 0;
}
5. P10721 [GESP202406 六级] 计算得分
线性DP+字符串。
字符串中的每个子串abc都可以被视为一个得分点,其余不等于abc的子串都直接无视,要考虑的只是如何合并这些abc使得分最大化,这就变为一个典型的线性DP。
定义f[i]表示字符串前i个字符所能取到的最大分数,遍历每个字符。
如果出现一个长度为3的子串等于abc,那么继续判断还有没有与其连接的abc(最好倒序判断,防止重复转移),据此转移状态;否则跳出多个abc连接的寻找过程。
状态转移方程是f[i]=max(f[(j-3<0)?0:j-3]+a[k]),当且仅当s.substr(j-2,3)=="abc"。
使用(j-3<0)?0:j-3的目的是防止数组出现负数下标,需要格外注意。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=22,M=1e5+5;
int n,m,a[N],f[M];
string s;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cin>>m;
cin>>s;
if(s.substr(0,3)=="abc")f[2]=a[1];
for(int i=3;i<m;i++){
f[i]=f[i-1];
for(int j=i,k=1;j-2>=0&&k<=n;j-=3,k++){
if(s.substr(j-2,3)=="abc"){
f[i]=max(f[i],f[(j-3)<0?0:(j-3)]+a[k]);
}else break;
}
}
cout<<f[m-1];
return 0;
}
9月4日
1. P10250 [GESP样题 六级] 下楼梯
不想说了,太水。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=62;
int n,f[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
f[1]=1,f[2]=2,f[3]=4;
for(int i=4;i<=n;i++){
f[i]=f[i-1]+f[i-2]+f[i-3];
}
cout<<f[n];
return 0;
}
2. P11963 [GESP202503 六级] 环线
滑动窗口。
首先断环为链,维护这个二倍链的前缀和。
维护一个单调队列,对于每一个1<=i<=2*n,维护一个滑动窗口,寻找长度在n之内的最大前缀和数组值。
注意特判全是负数的情况,否则只有70pts。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=2e5+2;
int n,a[2*N],sum[2*N],f[2*N],ans=-1e18,maxn=-1e18;
bool flag;
deque<int>dq;
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i]>=0)flag=1;
maxn=max(maxn,a[i]);
a[i+n]=a[i];
}
if(!flag){
cout<<maxn;
return 0;
}
for(int i=1;i<=2*n;i++){
sum[i]=sum[i-1]+a[i];
}
for(int i=1;i<=2*n;i++){
while(!dq.empty() && dq.front()<i-n){
dq.pop_front();
}
while(!dq.empty() && sum[dq.back()]>=sum[i]){
dq.pop_back();
}
dq.push_back(i);
if(sum[i]-sum[dq.front()]>ans){
ans=sum[i]-sum[dq.front()];
}
}
cout<<ans;
return 0;
}
3. P2170 选学霸
0-1背包+并查集。
首先对于所有实力相当的人,可以维护一个并查集f;再维护一个数组s,存储每个集合内的元素个数。
接下来的DP只需根据p的值转移状态即可。
对于每个并查集,都有选或不选两种选择,不能选一半(否则会抗议)。这就是一个典型的0-1背包,价值和体积相等。背包容量应从n开始遍历,而非m,这点要注意。
最后,对于每个dp[i],寻找abs(dp[i]-m)最小的值,输出即可。注意特判,输出0。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=2e4+10;
int n,m,k,f[N],p[N],s[N],dp[N],tot,ans=1e18,minn=1e18;
int find(int x){
if(f[x]==x)return x;
else return f[x]=find(f[x]);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>k;
if(m==0){
cout<<0;
return 0;
}
for(int i=1;i<=n;i++){
f[i]=i,p[i]=1;
}
while(k--){
int x,y,u,v;
cin>>x>>y;
u=find(x),v=find(y);
if(u!=v)f[u]=v,p[v]+=p[u];
}
for(int i=1;i<=n;i++){
if(f[i]==i){
s[++tot]=p[i];
}
}
for(int i=1;i<=tot;i++){
for(int j=n;j>=s[i];j--){
dp[j]=max(dp[j],dp[j-s[i]]+s[i]);
}
}
for(int i=1;i<=n;i++){
if(minn>abs(dp[i]-m)){
minn=abs(dp[i]-m);
ans=dp[i];
}
}
cout<<(ans!=1e18)*ans;
return 0;
}
9月5日
复习背包DP模板,准备下一周的背包学习。

浙公网安备 33010602011771号