2023年12月做题纪要
CF372C Watching Fireworks is Fun
12.15
学DP优化了。
设 \(f_{i,j}\) 表示在第 \(i\) 个时间,在第 \(j\) 个位置时的最大答案。
容易写出朴素的状态转移方程。
这里的 \(k\) 有一定的范围,
发现要枚举 \(i,j,k\) 可以考虑去掉 \(k\),显然当 \(i,j\) 确定的时候,状态转移方程可以变形成如下形式。
我们知道 \(f_{i,k}\) 是连续的一段,也就是区间RMQ问题,考虑单调队列优化。
数据要开long long,然后空间会炸,滚一下就行了。
点击查看代码
#include<bits/stdc++.h>
#define int long long
inline int read(){
int x=0,f=1;char ch=getchar();
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<1)+(x<<3)+(ch^48);
return x*f;
}
const int N=150005,M=305;
int n,m,t[M],b[M],a[M],q[N],d,head,tail,f[2][N];
signed main(){
memset(f,-0x3f,sizeof(f));
n=read(),m=read();d=read();
for(int i=1;i<=m;++i)a[i]=read(),b[i]=read(),t[i]=read();
int zc=0;
for(int i=1;i<=n;++i)
f[zc][i]=b[1]-abs(a[1]-i);
for(int i=2;i<=m;++i,zc^=1){
head=1,tail=0;
for(int j=1;j<=std::min(n,(t[i]-t[i-1])*d+1);++j){
while(head<=tail&&f[zc][j]>=f[zc][q[tail]])tail--;
q[++tail]=j;
}
f[zc^1][1]=f[zc][q[head]]+b[i]-abs(a[i]-1);
for(int j=2;j<=n;++j){
int l=std::max(j-(t[i]-t[i-1])*d,1ll),r=std::min(j+(t[i]-t[i-1])*d,n);
while(head<=tail&&f[zc][r]>=f[zc][q[tail]])tail--;
q[++tail]=r;
if(q[head]<l)head++;
f[zc^1][j]=f[zc][q[head]]+b[i]-abs(j-a[i]);
}
}
long long ans=-2e9;
for(int i=1;i<=n;++i)ans=std::max(ans,f[zc][i]);
std::cout<<ans;
}
CF327C Magic Five
12.16
HZOI-Isaac跟我说昨天题号写错了,372写成了327,(对不起,我唐),然后他发了一个327的题解,我就开了做做。
首先 \(5\) 的倍数要求末尾是 \(0\) 或 \(5\) 所以我们只用看给定字符串的 \(0\) 和 \(5\) 就好,我们钦定他是最终的数的末尾。
在他之前的选择删掉,在他之后的全部删掉,方案数就是 \(2^{pow-1}\),答案累加就可以了。
容易想到一个朴素的,每次找到后,循环 \(n\) 遍,\(ans=\sum^{len}_{pos}{\sum_{k=0}^{n-1} {2^{pos+k\cdot len-1}}}\),这样复杂度 \(O(nm)\),肯定不行。
不难看出上面的式子是一个等比数列,直接求和就行,最终式子是
k是系数,提前处理出来就好,\(k=\dfrac{2^{len\cdot n}-1}{2^{len}-1}\) 然后就做完了。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
const ll mod=1e9+7,N=1e5+5;
char s[N];
ll ans,len,ny,n,xs;
inline ll qpow(ll a,ll b){
ll ans=1;
for(;b;b>>=1,a=a*a%mod)if(b&1)ans=ans*a%mod;
return ans;
}
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);
std::cin.tie(0),std::cout.tie(0);
std::cin>>s+1>>n;
len=strlen(s+1);
ll k=qpow(2,len);
ny=qpow(k-1,mod-2);
xs=(qpow(k,n)-1)*ny%mod;
for(int i=1;i<=len;++i)
if(s[i]=='0'||s[i]=='5')
ans=(ans+xs*qpow(2,i-1)%mod)%mod;
std::cout<<ans<<'\n';
}
取模放括号里死活没看出来。
P2254 [NOI2005] 瑰丽华尔兹
12.17
还是先朴素,设 \(f_{t\ i\ j\ d}\) 表示在第 \(t\) 个时刻,在 \(i,j\) 位置上,上一步是停留还是滑动的最大步数。这个就是四个方向随便转移。
\(T_{max}=4*10^4\) 这么做肯定不行。发现 \(k\) 很小,只有 \(200\) ,所以不妨让 \(k\) 做状态,这样时间空间都可以接受了,这时候的转移就是从 \(len_i\) 个距离以内的方向转移,然后显然这时候的停留和滑动的状态已经没用了,所以最后 \(f_{t\ i\
j}\) 表示在第 \(t\) 个时间段,在第 \(i,j\) 个位置时的最大步数,状态转移方程如下:
这里 \(k\) 是有范围的:\(i-len\le k\le i\) ,这个方程和范围只针对方向为上时,其他三个同理,不多赘述。
点击查看代码
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=205;
bool D[N][N];
int n,m,x,y,k,f[2][N][N],now,l[N],r[N],dir[N],q[N],head,tail;
int main(){
// freopen("in.in","r",stdin);freopen("out.out","w",stdout);
std::cout.tie(0);
n=read(),m=read(),x=read(),y=read(),k=read();
memset(f,-0x3f,sizeof(f));
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
char ch=getchar();
while(ch!='.'&&ch!='x')ch=getchar();
if(ch=='x')D[i][j]=true;
}
for(int i=1;i<=k;++i)l[i]=read(),r[i]=read(),dir[i]=read();
f[now][x][y]=0;
for(int t=1;t<=k;++t,now^=1){
int len=r[t]-l[t]+1;
if(dir[t]==1){
for(int j=1;j<=m;++j){
head=1,tail=0;
for(int i=n;i;--i){
if(D[i][j]){head=1,tail=0;continue;}
while(head<=tail&&f[now][i][j]-abs(i-n)>=f[now][q[tail]][j]-abs(q[tail]-n))tail--;
q[++tail]=i;
if(abs(q[head]-i)>len)head++;
f[now^1][i][j]=f[now][q[head]][j]-abs(q[head]-n)+abs(i-n);
}
}
}
if(dir[t]==2){
for(int j=1;j<=m;++j){
head=1,tail=0;
for(int i=1;i<=n;++i){
if(D[i][j]){head=1,tail=0;continue;}
while(head<=tail&&f[now][i][j]-abs(i-1)>=f[now][q[tail]][j]-abs(q[tail]-1))tail--;
q[++tail]=i;
if(abs(q[head]-i)>len)head++;
f[now^1][i][j]=f[now][q[head]][j]-abs(q[head]-1)+abs(i-1);
}
}
}
if(dir[t]==3){
for(int i=1;i<=n;++i){
head=1,tail=0;
for(int j=m;j;--j){
if(D[i][j]){head=1,tail=0;continue;}
while(head<=tail&&f[now][i][j]-abs(j-m)>=f[now][i][q[tail]]-abs(q[tail]-m))tail--;
q[++tail]=j;
if(abs(q[head]-j)>len)head++;
f[now^1][i][j]=f[now][i][q[head]]-abs(q[head]-m)+abs(j-m);
}
}
}
if(dir[t]==4){
for(int i=1;i<=n;++i){
head=1,tail=0;
for(int j=1;j<=m;++j){
if(D[i][j]){head=1,tail=0;continue;}
while(head<=tail&&f[now][i][j]-abs(j-1)>=f[now][i][q[tail]]-abs(q[tail]-1))tail--;
q[++tail]=j;
if(abs(q[head]-j)>len)head++;
f[now^1][i][j]=f[now][i][q[head]]-abs(q[head]-1)+abs(j-1);
}
}
}
}
int ans=-1;
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)ans=std::max(ans,f[now][i][j]);
std::cout<<ans<<'\n';
}
是一道挺好的单调队列优化DP
P2569 [SCOI2010] 股票交易
12.19-12.20
直接设 \(f_{i\ j}\) 表示在第 \(i\) 天,手上还剩 \(j\) 个股票时的最大收入。
容易写出状态转移方程:\(f_{i\ j}=max\{f_{k\ t}+(t-j)\cdot w\}\),这样不好看,我们可以拆成这样的形式:
这样 \(max\) 里面的东西与 \(i\) 和 \(j\) 无关,显然单调队列优化。
这里的 \(k,t,w\) 都有限定,\(k\le i-len+1\qquad j-as_i\le t\le j+bs_i\),\(w\) 的取值取决于这个状态是买入转移过来的还是卖出转移过来的。
我们发现 \(f_{k\ t}\) 的取值范围实际上是一个只有长不断伸缩而宽递增的一个矩形(以 \(i\) 为宽,以 \(j\) 为长)。
这样就很优,因为这样我们纵向的最值随便维护,横向的直接那单调队列。(一开始我还以为是理想的正方形)
直接转移还是不行的,因为这样相当于每天只进行一次买或卖的转移,但是买和卖能够同时转移。
我们考虑开一个辅助数组 \(zc\),现将某天卖股票的转移放入 \(zc\),在考虑从 \(zc\) 由买股票的转移放入 \(f\) 数组,先转移卖可以确保不会超出股票数目最大限制。
\(DP\) 结束后,我们找后 \(W+1\) 天的剩余 \(0\) 股票时的最值就好。
点击查看代码
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=2005;
int T,MaxP,W,AP[N],BP[N],AS[N],BS[N],headl=1,taill=0,headr=1,tailr=0,f[N][N],ql[N],qr[N],zc[N],maxq[N];
int main(){
// freopen("in.in","r",stdin);freopen("out.out","w",stdout);
T=read(),MaxP=read(),W=read();
memset(f,-0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=T;++i)
AP[i]=read(),BP[i]=read(),AS[i]=read(),BS[i]=read();
for(int i=1;i<=T;++i){
headl=headr=1,taill=tailr=0;
int ri=std::max(0,i-W-1);
for(int j=0;j<=MaxP;++j)
if(f[ri][j]>=f[maxq[j]][j])maxq[j]=ri;
for(int j=0;j<=std::min(BS[i],MaxP);++j){
while(headr<=tailr&&f[maxq[j]][j]+j*BP[i]>=f[maxq[qr[tailr]]][qr[tailr]]+qr[tailr]*BP[i])tailr--;
qr[++tailr]=j;
}
for(int j=0;j<=MaxP;++j){
int r=std::min(MaxP,j+BS[i]);
while(headr<=tailr&&f[maxq[r]][r]+r*BP[i]>=f[maxq[qr[tailr]]][qr[tailr]]+qr[tailr]*BP[i])tailr--;
qr[++tailr]=r;
if(qr[headr]<j)headr++;
zc[j]=f[maxq[qr[headr]]][qr[headr]]+(qr[headr]-j)*BP[i];
}
for(int j=0;j<=MaxP;++j){
int l=std::max(0,j-AS[i]);
while(headl<=taill&&zc[j]+j*AP[i]>=zc[ql[taill]]+ql[taill]*AP[i])taill--;
ql[++taill]=j;
if(ql[headl]<l)headl++;
f[i][j]=zc[ql[headl]]-(j-ql[headl])*AP[i];
}
}
int ans=0;
for(int i=T-W;i<=T;++i)ans=std::max(f[i][0],ans);
std::cout<<ans<<'\n';
}
- 这题感觉没那么难,但是我中途有一车唐氏操作,心路历程放今天的日记了。
P2627 [USACO11OPEN] Mowing the Lawn G
(题做不出来了,补一篇题解)
简单题,和P2034重了
题意:给定一行 \(n\) 个非负整数 \(a_1 \cdots a_n\)。现在你可以选择其中若干个数,但不能有超过 \(k\) 个连续的数字被选择。你的任务是使得选出的数字的和最大。(直接抄的2034)
正着考虑发现很麻烦,不知道咋维护,但是我们如果考虑删去的话就很有前途,正难则反。
设 \(f_i\) 表示前 \(i\) 个数合法状态的最小删除和,方程 \(f_i=max\{a_t+f_{t-1}\}\)
到 \(i\) 位置时,肯定选出一个位置 \(t\) 的数字删去,\(i-k\le t\le i\),选完之后,我们还要保证选的 \(t\) 位置之前是合法状态,所以再加上 \(f_{t-1}\),拿单调队列优化就做完了。

浙公网安备 33010602011771号