您的位置:首页 > 其它

sc2017新高二&高一模拟赛3 总结

2017-08-05 10:20 393 查看

T1:通关游戏(SMOJ2007)

题目分析:我们分析一下就会发现,对于一个块,主角肯定是从左到右按顺序打通关的。而且由于其选择的概率是tis,所需要的时间便是sti。我们可以考虑DP,记i个任务划分成k段的最小期望时间为f[i][k],那么我们可以枚举上一段的结束位置j,然后划分成子问题。然后做完中间的那些任务的时间为:1+tj+1+tj+2tj+2+tj+1+tj+2+tj+3tj+3+……tj+1+……+titi

这很明显可以用前缀和优化,记s为t的前缀和,则原式转化为:

∑w=j+1is[w]−s[j]t[w]=∑w=j+1is[w]t[w]−∑w=j+1i1t[w]s[j]

我们不妨再记A为st的前缀和,记B为1t的前缀和。那么DP的状态转移方程为:

f[i][k]=f[j][k−1]−A[j]+B[j]s[j]−B[i]s[j]+A[i]

这样朴素的DP时间是O(n2k)的,很明显需要优化。我们发现式子的左边三个部分只跟j有关,中间有一项既跟i又跟j有关,最右边那一项在枚举了i之后是个定值,这让我们想到了斜率优化。我们记Y[j]=f[j][k-1]-A[j]+B[j]s[j],X[j]=s[j],则我们考虑j2比j1优的条件(假设j2在j1之后):

Y[j2]−X[j2]∗B[i]<Y[j1]−X[j1]∗B[i]

由于x[j2]-x[j1]>0,上式等价于:

Y[j2]−Y[j1]X[j2]−X[j1]<B[i]

我们记左边的那一部分为kj1j2,如果有kj0j1大于它,那么不管B[i]取值多少,j1必定不优,即可弹出(可以自己画个图YY一下)。这告诉我们应该维护的是一个下凸壳,又由于B[i]单调递增,可以用斜率优化。时间O(nk)。

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=200100;
const int maxk=52;
const double eps=1e-13;

double f[maxn][maxk];
double A[maxn];
double B[maxn];
double C[maxn];

int que[maxn];
int head,tail;

long long sum[maxn];
int t[maxn];
int n,k;

void Push(int x,int j)
{
que[++tail]=x;
double Yx=f[x][j]+C[x]-A[x];
double Xx=(double)sum[x];
while (head+1<tail)
{
int y=que[tail-1];
int z=que[tail-2];
double Yy=f[y][j]+C[y]-A[y];
double Xy=(double)sum[y];
double Yz=f[z][j]+C[z]-A[z];
double Xz=(double)sum[z];
double temp1=(Yx-Yy)*(Xy-Xz);
double temp2=(Yy-Yz)*(Xx-Xy);
if ( temp1-temp2>eps ) break;
tail--;
que[tail]=que[tail+1];
}
}

bool Judge(int j,int i)
{
if (head>=tail) return false;
int x=que[head];
int y=que[head+1];
double Yx=f[x][j]+C[x]-A[x];
double Xx=(double)sum[x];
double Yy=f[y][j]+C[y]-A[y];
double Xy=(double)sum[y];
double temp1=Yy-Yx;
double temp2=B[i]*(Xy-Xx);
return (temp1-temp2<eps);
}

int main()
{
freopen("2007.in","r",stdin);
freopen("2007.out","w",stdout);

scanf("%d%d",&n,&k);
for (int i=1; i<=n; i++) scanf("%d",&t[i]);
sum[1]=t[1];
for (int i=2; i<=n; i++) sum[i]=sum[i-1]+(long long)t[i];
for (int i=1; i<=n; i++) A[i]=(double)sum[i]/(double)t[i];
for (int i=2; i<=n; i++) A[i]+=A[i-1];
for (int i=1; i<=n; i++) B[i]=1.0/(double)t[i];
for (int i=2; i<=n; i++) B[i]+=B[i-1];
for (int i=1; i<=n; i++) C[i]=B[i]*(double)sum[i];

f[0][1]=0.0;
for (int i=1; i<=n; i++) f[i][1]=A[i];
for (int j=2; j<=k; j++)
{
head=1,tail=0;
for (int i=j; i<=n; i++)
{
Push(i-1,j-1);
while ( Judge(j-1,i) ) head++;
int last=que[head];
double temp=C[last]-A[last]-B[i]*(double)sum[last];
f[i][j]=f[last][j-1]+A[i]+temp;
}
}

printf("%.10lf\n",f
[k]);
return 0;
}


T2:长路(SMOJ2072)

题目分析:这一题主要是要分析出一个结论:当到达某个点i的时候,1~i-1房间里的叉的个数一定是偶数。这个嘛……自己YY一下就行了。假设有一个之前的房间叉的标记不是偶数,那么人一定不会走到下一个房间。又由于顶多只能从i走到i+1,无法往后跳两个房间以上,所以每一个房间都要是偶数。也就是说,我们只要记忆化一下每个房间走第一个门走要多久才能再回到这里,记为f[i]。要算新的f[i]的时候,将f[ p[i] ]+1~f[i-1]+1全部加进来再+1即可,因为走到i之后,要先用一步走到p[i],然后走完f[ p[i] ]步,再走一步到达p[i]+1,然后走f[ p[i]+1 ]步……依此类推。这个部分用前缀和算的话,总的时间就是O(n)。

CODE:

#include<iostream>
#include<string>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=1000100;
const int M=1000000007;

int p[maxn];
int f[maxn];
int sum[maxn];
int n;

int main()
{
freopen("2072.in","r",stdin);
freopen("2072.out","w",stdout);

scanf("%d",&n);
for (int i=1; i<=n; i++) scanf("%d",&p[i]);
f[1]=1;
sum[1]=2;
for (int i=2; i<=n; i++)
{
f[i]=1;
//for (int j=p[i]; j<i; j++) f[i]=(f[i]+f[j]+1)%M;
f[i]=(f[i]+sum[i-1]-sum[ p[i]-1 ]+M)%M;
sum[i]=(sum[i-1]+f[i]+1)%M;
}
int ans=0;
for (int i=1; i<=n; i++) ans=(ans+f[i]+1)%M;
printf("%d\n",ans);

return 0;
}


T3:小Z的数组

题目分析:这题算是比较有趣的一题,它使我重新认识到了线段树这种普通的一维数据结构也能维护很多东西……

一开始我认为这题的区间修改在线段树上无法合并懒惰标记,于是我就往分块那方面想。修改的时候,对于不是一整个块的部分,我们直接暴力加,难点在于如何O(1)地对一整个块的部分进行处理。



假设某个块的第一个数要加a2,我们先对这个块开一个懒惰标记lazy+=a2,表示这整个块的数至少都加了lazy。然后从第二个数开始,要多加的部分val是没有规律的。但如果我们对其进行差分,出来的dec是一个等差数列,我们只要记住其首项st和公差d即可。之后查询的时候可以用O(n√)推出整个dec数列,从而知道val数列。还有一个重点是:等差数列的加法相当于将两个首项和公差分别相加,这个信息是可以合并的!同时为了方便查询,我们还要用一个sum数组维护一个块的和,在修改的时候用平方前缀和公式∑ni=1i2=i(i+1)(2i+1)6即可O(1)维护。

现在考虑查询:对于跨越一整个块的部分,直接调用sum数组;对于非整块的部分,用O(n√)的时间推一遍每个数是多少,然后累加即可。

后来LZH大佬给出了正解的做法:



我们先在线段树上的代表区间[L,L+k]的节点维护一个val值,它等于L2+(L+12)+……+(L+k)2,并用一个值记录一下val被加了几次。然后当要对这个区间加一个d2~(d+k)2的时候,先记为多加了一次val,将其对val作差,得到一个等差数列。然后像上面分块的做法一样,维护一个st和d值即可。并且我们还要维护一个sum值,记录区间的总和。下放标记的时候这些信息显然是可以正确维护的,讨论一下就可以了。

CODE(分块):

#include<iostream>
#include<string>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=100100;
const int maxm=330;
const long long M=1000000007;
typedef long long LL;

LL sum[maxm];
LL lazy[maxm];

LL st[maxm];
LL d[maxn];

LL add[maxn];
LL X[maxn];

int n,m,sn;

int Get(int x)
{
return (x-1)/sn+1;
}

LL Sum(LL x)
{
LL y=x+1,z=(x<<1)|1;
bool f2=false,f3=false;
if ( !(x&1) && !f2 ) x>>=1,f2=true;
if ( !(x%3) && !f3 ) x/=3,f3=true;
if ( !(y&1) && !f2 ) y>>=1,f2=true;
if ( !(y%3) && !f3 ) y/=3,f3=true;
if ( !(z&1) && !f2 ) z>>=1,f2=true;
if ( !(z%3) && !f3 ) z/=3,f3=true;
LL temp=x*y%M*z%M;
return temp;
}

void Work(int L,int R,LL a)
{
while ( L%sn!=1 && L<=R )
{
LL b=a*a%M;
add[L]+=b;
if (add[L]>=M) add[L]-=M;
int id=Get(L);
sum[id]+=b;
if (sum[id]>=M) sum[id]-=M;
L++,a++;
}
if (L>R) return;

while (L+sn-1<=R)
{
int id=Get(L);
LL b=(a<<1)|1;
if (b>=M) b-=M;
st[id]+=b;
if (st[id]>=M) st[id]-=M;
d[id]+=2;

b=a*a%M;
lazy[id]+=b;
if (lazy[id]>=M) lazy[id]-=M;

LL temp=Sum(a+sn-1)-Sum(a-1);
if (temp<0) temp+=M;
sum[id]+=temp;
if (sum[id]>=M) sum[id]-=M;
L+=sn,a+=sn;
}

while (L<=R)
{
LL b=a*a%M;
add[L]+=b;
if (add[L]>=M) add[L]-=M;
int id=Get(L);
sum[id]+=b;
if (sum[id]>=M) sum[id]-=M;
L++,a++;
}
}

int Calc(int x,int t)
{
int h=(x-1)*sn+1;
if (t<=h) return 0;
LL sum=0,val=0,now=st[x];
for (int i=h+1; i<=t; i++)
{
val+=now;
if (val>=M) val-=M;
sum+=val;
if (sum>=M) sum-=M;
now+=d[x];
if (now>=M) now-=M;
}
return sum;
}

int Query(int L,int R)
{
LL temp=X[R]-X[L-1];
if (temp<0) temp+=M;

int k=L;
while (k%sn) k++;
int id=Get(L);
temp+=Calc(id, min(k,R) );
if (temp>=M) temp-=M;
temp-=Calc(id,L-1);
if (temp<0) temp+=M;

while ( L<=k && L<=R )
{
temp+=add[L];
if (temp>=M) temp-=M;
temp+=lazy[id];
if (temp>=M) temp-=M;
L++;
}
if (L>R) return temp;

while (L+sn-1<=R)
{
id=Get(L);
temp+=sum[id];
if (temp>=M) temp-=M;
L+=sn;
}

id=Get(L);
temp+=Calc(id,R);
if (temp>=M) temp-=M;
while (L<=R)
{
temp+=add[L];
if (temp>=M) temp-=M;
temp+=lazy[id];
if (temp>=M) temp-=M;
L++;
}
return (int)temp;
}

int main()
{
freopen("2137.in","r",stdin);
freopen("2137.out","w",stdout);

scanf("%d%d",&n,&m);
for (int i=1; i<=n; i++) scanf("%I64d\n",&X[i]);
for (int i=2; i<=n; i++) X[i]=(X[i]+X[i-1])%M;

sn=(int)floor( sqrt( (double)n )+1e-6 );
for (int i=1; i<=m; i++)
{
int x;
scanf("%d",&x);
if (x==1)
{
int l,r,a;
scanf("%d%d%d",&l,&r,&a);
Work(l,r,a);
}
if (x==2)
{
int l,r;
scanf("%d%d",&l,&r);
int ans=Query(l,r);
printf("%d\n",ans);
}
}

return 0;
}


总结:最近这几场比赛我都考得不好……其实这次的比赛我是可以拿到225分的,然而我却只有145。T1由于斜率优化敲得不熟,用了1.5h。T2的话我并没有推出那个结论,导致我认为每一次返回的时候房间里叉的个数的奇偶性不同,无法记忆化,怎么也想不出来。现在想想,其实只要画几个例子,应该就能推出这个结论,而且考试的时候很多人提交了这题,说明并不难。然而我却连画例子这个最基本的方法都忘了QAQ。想了30min后我转去了T3,用了15min就想到了分块的方法(果然我还是更加擅长数据结构题)。然而分块的代码我写得很丑,花了50min才敲出来,过了样例和手出的两组数据就交了,结果我第三组小数据出错了,还剩下10min给我调试……然后我就炸了。以后我还是要提高编码能力,对于一些陌生的知识点查漏补缺,考试的时候才能更快地写出调出程序。对于暂时想不到的题目,可以画一些例子,从中找出些规律从而解题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: