[Codeforces #172] Tutorial

Link:

Codeforces #172 传送门

A:

一眼看上去分两类就可以了

1、每个矩形只有两条边相交,重合的形状为菱形

2、每个矩形四条边都有相交

对于情况1答案为$h*h/sin(a)$

对于情况2可以列出一个二元一次方程组,手动解一下就好了

 

不过计算几何确实容易写挂啊……

有几个注意点:

1、对于情况1$h$指的是较短的边,不符合时要交换$w,h$

以后处理长方形时还是要注意,不能只看样例啊

2、在判断情况1,2的临界点时用$sin$来判断,因为此时$a$已经全转为锐角了

#include <bits/stdc++.h>

using namespace std;
#define X first
#define Y second
#define PI M_PI
typedef long long ll;
typedef pair<int,int> P;
double w,h,a,x,y,res;

int main()
{
    scanf("%lf%lf%lf",&w,&h,&a);
    if(!a||a>=180) return printf("%.9lf",w*h),0;    
    if(a>90) a=180-a;
    if(h>w) swap(w,h);//情况1要求h是短边 
    a=a*PI/180;
    if(sin(a)>=sin(2*atan2(w,h)))//由于角度已改变,用sin判断 
        res=h*h/sin(a);
    else
        y=(h*(cos(a)+1)-w*sin(a))/((cos(a)+1)*(cos(a)+1)-sin(a)*sin(a)),
        x=(h-y*(cos(a)+1))/sin(a),res=w*h-(x*x+y*y)*sin(a)*cos(a);
    printf("%.9lf",res);
    return 0;
}
Problem A

其它方法:

1、现场大部分人都是暴力半平面交做的……

如果怕少考虑特解这样确实能大大降低写挂概率

2、对于情况2也可以不用解方程

其中三角形的直角边和长/宽一半的差是能直接算的,这样好像更简单一些

 

B:

一开始全在想异或性质的应用……

但后来发现此题仅和如何枚举所有区间的最大+次大值有关

其实就是要想到一种能滤去所有“无用区间”的方式

 

考虑无用区间产生的原因就是往最大/次大值两侧扩展了更小的值

如果考虑当前值所有可行的次大值,则其一定是从右向左递增的

这样用一个单调栈维护,每次新加进一个数和 比其小的数+第一个比其大的数 的异或值更新答案

#include <bits/stdc++.h>

using namespace std;
#define X first
#define Y second
typedef long long ll;
typedef pair<int,int> P;
const int MAXN=1e5+10;
int n,x,st[MAXN],top,res;

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        while(top&&x>st[top])
            res=max(res,x^st[top--]);
        if(top) res=max(res,x^st[top]);
        st[++top]=x;
    }
    printf("%d",res);
    return 0;
}
Problem B

 

C:

感觉自己还是有期望恐惧症啊……

此题实为一个结论题,结论为$\sum \frac{1}{dep[i]}$

 

这其实是利用了期望是线性的这一性质,即$E(x+y)=E(x)+E(y)$

有了该性质就可以单独考虑每个点被直接删除的期望,再相加即可

一个点最终被删除当且仅当其到根的路径上的任意一点被删除

因此其直接删除该点的期望为$\frac{1}{dep[i]}$,最后再求和

#include <bits/stdc++.h>

using namespace std;
const int MAXN=1e5+10;
vector<int> G[MAXN];
int n,x,y;double res;

void dfs(int x,int anc,int dep)
{
    res+=1.0/dep;
    for(int i=0;i<G[x].size();i++)
        if(G[x][i]!=anc) dfs(G[x][i],x,dep+1);
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<n;i++)
        scanf("%d%d",&x,&y),G[x].push_back(y),G[y].push_back(x);
    dfs(1,0,1);
    printf("%.6lf",res);
    return 0;
}
Problem C

 

D:

此题将一类经典问题加上了修改和动态查询

该经典问题是指:在$n$个数中取$k$段,使得最终和最大

 

先考虑静态问题,发现是可以直接$dp$的

用$dp[i][j][0/1]$表示前$i$个数中取$j$段的最大和,同时1表示最后一个数必须取

这样的复杂度为$O(n*k)$

 

不过这个复杂度是可以用fhq提出的增量法优化的:

每次取当前和最大的一段,同时将该段中的数全部取反。重复$k$次

其中的取反操作其实就是提供了“反悔”机制,如果后面有一段跨过了取反的段就说明这一段不再取了

同时取反后的数依然满足求最大和的目的,毕竟加上了最大取反的数就是少减去了最大的数

对于上面两个操作明显可以用线段树来实现,复杂度是$O(k*log(n))$

这个方法同样可以用费用流来理解

如果对原问题建图将是$<S,i,1,0><i,T,1,0><i,i+1,1,a[i]>$

发现上面的增量法其实就是优化了跑该网络流的过程

 

接下来用上面的方法来解决动态问题

对于朴素的$dp$方法,对每个点记录$f[k][0/1/2/3]$

表示在该点表示的区间中取$k$段的最大值,限制分别为头必取/尾必取/头尾都取/无限制

但这样将两个区间合并需要$O(k^2)$,总复杂度为$O(q*k^2*log(n))$,想过要稍微卡卡常

 

但是增量法可以将总复杂度降为$O(q*k*log(n))$,原因在于线段树中每个点都只要保留取1段的最大值

每次的区间合并是$O(1)$的,不过对于每次查询要进行$k$次求最大和、区间取反、撤销

对于每个点记录$mx,lmx,rmx$分别表示取一段的最大和、最大前缀和、最大后缀和

#include <bits/stdc++.h>

using namespace std;
#define X first
#define Y second
#define mid ((l+r)>>1)
#define ls (k<<1)
#define rs (k<<1|1)
#define lc ls,l,mid
#define rc rs,mid+1,r
typedef long long ll;
typedef pair<int,int> P;
typedef double db;
const int MAXN=1e5+10;
int top,L[MAXN],R[MAXN];
int n,m,dat[MAXN],rev[MAXN<<2],op,l,r,k,res;
struct SGT
{
    int l,r,lm,rm;
    int lmx,rmx,mx,sum;
    void mdy(int val,int pos)
    {
        sum=val;
        l=r=lm=rm=pos;
        lmx=rmx=mx=(val>0?val:0);
    }
    void upd(SGT a,SGT b)
    {
        sum=a.sum+b.sum;
        lmx=a.lmx;lm=a.lm;rmx=b.rmx;rm=b.rm;
        if(a.mx>b.mx) mx=a.mx,l=a.l,r=a.r;
        else mx=b.mx,l=b.l,r=b.r;
        
        if(a.rmx+b.lmx>mx) mx=a.rmx+b.lmx,l=a.rm,r=b.lm;
        if(a.sum+b.lmx>lmx) lmx=a.sum+b.lmx,lm=b.lm;
        if(a.rmx+b.sum>rmx) rmx=a.rmx+b.sum,rm=a.rm;
    }
}seg[MAXN<<2][2],ret,tmp;

void Pushup(int k)
{
    for(int i=0;i<2;i++) 
        seg[k][i].upd(seg[ls][i^rev[ls]],seg[rs][i^rev[rs]]);
}
void Update(int pos,int val,int k,int l,int r)
{
    if(l==r)
    {
        seg[k][0].mdy(val,pos);
        seg[k][1].mdy(-val,pos);
        return;
    }
    if(pos<=mid) Update(pos,val,lc);
    else Update(pos,val,rc);
    Pushup(k);
}
void Modify(int a,int b,int k,int l,int r)
{
    if(a<=l&&r<=b){rev[k]^=1;return;}
    if(a<=mid) Modify(a,b,lc);
    if(b>mid) Modify(a,b,rc);
    Pushup(k);
}
void Query(int a,int b,int k,int l,int r,int f)
{
    f^=rev[k];//永久化标记 
    if(a<=l&&r<=b)
    {
        if(l==a) ret=seg[k][f];
        else tmp.upd(ret,seg[k][f]),ret=tmp;
        return;
    }
    if(a<=mid) Query(a,b,lc,f);
    if(b>mid) Query(a,b,rc,f);
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&dat[i]),Update(i,dat[i],1,1,n);
    scanf("%d",&m);
    while(m--)
    {
        scanf("%d%d%d",&op,&l,&r);
        if(op==0) dat[l]=r,Update(l,r,1,1,n);
        else
        {
            scanf("%d",&k);
            top=0;res=0;
            while(top<k)
            {
                Query(l,r,1,1,n,0);
                if(ret.mx<=0) break;
                res+=ret.mx;L[++top]=ret.l;R[top]=ret.r;
                Modify(L[top],R[top],1,1,n);
            }
            printf("%d\n",res);//对更改进行撤销 
            while(top) Modify(L[top],R[top],1,1,n),top--;
        }
    }
    return 0;
}
Problem D

此题有一些技巧是要注意的

1、翻转操作是可以用永久化标记的!

对每个点同时记录两种状态下的值,查询时用一个值记录当前实际是哪种状态即可

只要修改操作和值无关一般都可以不用$pushdown$,尽量用永久化标记

2、线段树对多段问题的更新上传

对于多段问题每个节点都要记录两个边界是否选取来确定合并之后的段数,算是个套路吧

 

E:

$Seter$出的神题……

普通的$dp$非常好想,用$dp[i][j]$表示第$i$个数选$j$的代价,$f[i][j]$表示$min(dp[i-1][j-k]),k\in[a,b]$

那么$dp[i][j]=2*(j-dat[i])+f[i][j]$

但其时间/空间复杂度都和数的值域相关,需要优化

接下来就是神级的单调性优化了

 

为了将$dp$和值域去相关,可以将$dp[i],f[i]$看成和$j$相关的函数!

接下来对$dp[i],f[i]$求导,同时由于$dp[1]'$单调,假设$dp[i]'$单调,且零点为$k$,则:

$dp[i+1]'(j)=2*(j-dat[i+1])+f[i+1]'(j)$,$f[i+1]'(j)=dp[i]'(j-a)(j<k+a),0(k+a\le j\ge k+b),dp[i]'(j-b)(j>k+b)$

可以发现每次$f[i+1]'$是将$dp[i]'$的函数中$[1,k)$向右移$a$,$(k,q]$向右移$b$,然后中间再加上一段零

而每次$dp[i+1]'$是在$f[i+1]'$的基础上加上一个单调递增的函数,因此$dp[i]'$单调增则$dp[i+1]'$单调增

 

证明了$dp[i]'$的单调性,且发现每次在原函数的基础上仅增加了一段一次函数

因此只要模拟上述过程,记录至多$n$段函数,就能最终在$O(n)$的时间内找到零点,即当前最优解

接下来考虑如何输出方案:

对于每个$i$记录下当前的零点位置

在求出$n$的零点后向回逆推每次的选择即可

#include <bits/stdc++.h>

using namespace std;
#define X first
#define Y second
typedef long long ll;
typedef double db;
typedef pair<db,db> P;
const int MAXN=1e4+10;
P dp[MAXN*2];int n,cp,tot;
db q,a,b,zp,dat[MAXN],res[MAXN],sum;

int main()
{
    scanf("%d%lf%lf%lf",&n,&q,&a,&b);
    for(int i=1;i<=n;i++) scanf("%lf",&dat[i]);
    dp[++tot]=P(1,2*(1-dat[1]));
    dp[++tot]=P(q+1,2*(q+1-dat[1]));
    res[1]=zp=dat[1];cp=1;
    for(int i=2;i<=n;i++)
    {
        for(int j=tot;j>cp;j--) dp[j+2]=dp[j];
        tot+=2;dp[cp+1]=P(zp+a,0);dp[cp+2]=P(zp+b,0);
        
        for(int j=1;j<=cp;j++) dp[j].X+=a;
        for(int j=cp+3;j<=tot;j++) dp[j].X+=b;
        for(int j=1;j<=tot;j++) dp[j].Y+=2*(dp[j].X-dat[i]);
        
        if(dp[1].Y>=0) cp=0,zp=dp[1].X;
        else
        {//找零点 
            for(cp=1;cp<tot;cp++)
                if(dp[cp].Y<=0&&dp[cp+1].Y>0) break;
            zp=dp[cp].X-dp[cp].Y*(dp[cp+1].X-dp[cp].X)/(dp[cp+1].Y-dp[cp].Y);
        }
        res[i]=min(zp,q);
    }
    
    for(int i=n-1;i;i--)
        if(res[i]+a>res[i+1]) res[i]=res[i+1]-a;
        else if(res[i]+b<res[i+1]) res[i]=res[i+1]-b;
    for(int i=1;i<=n;i++)
        printf("%.6lf ",res[i]),sum+=(res[i]-dat[i])*(res[i]-dat[i]);
    printf("\n%.6lf",sum);
    return 0;
}
Problem E

对于$dp$模型和值域相关的题目可以考虑这样的函数式优化

如果能证明函数为单峰函数就非常方便了

同时有时候在值域很大时求单峰函数最值不能直接用三分法,会TLE(如此题)

这时就要从导数的角度考虑,看看导数有没有什么性质能不需要二分(此题导数为分段形式,记录端点来优化)

可以考虑将每个$dp[i]$和$dp[i]'$的函数图像画出来方便猜结论

 

 

posted @ 2018-08-25 16:54  NewErA  阅读(261)  评论(0编辑  收藏  举报