Topcoder SRM 616 Div2 1000 TwoLLogo

Task:
给定一个 nm 的平面图,由’.’与’#’构成,现要在’.’处画出两个 L 形图案,要求两个L不能相交,求方案数,答案对 P 取模。(n<=1000,P=1000000007)
Solution:
一个 L 由四个数字坐标决定,因此最朴素的做法即是枚举两个L,复杂度为 O(n8) ,期望得分为30分。

bool judge(int x1,int y1,int l1,int r1,int x,int y){
    if(x==l1){
        if(y>=y1&&y<=r1)return true;
    }
    if(y==y1){
        if(x>=x1&&x<=l1)return true;
    }
    return false;
}
struct P30{
    void solve(){
        ll ans=0;
        for(int x1=1;x1<=n;x1++)
            for(int y1=1;y1<=m;y1++){
                if(str[x1][y1]=='#')continue;
                for(int l1=x1+1;l1<=n;l1++){
                    if(str[l1][y1]=='#')break;
                    for(int r1=y1+1;r1<=m;r1++){
                        if(str[l1][r1]=='#')break;
                        for(int x2=1;x2<=n;x2++)
                            for(int y2=1;y2<=m;y2++){
                                if(str[x2][y2]=='#'||judge(x1,y1,l1,r1,x2,y2))continue;
                                for(int l2=x2+1;l2<=n;l2++){
                                    if(str[l2][y2]=='#'||judge(x1,y1,l1,r1,l2,y2))break;
                                    for(int r2=y2+1;r2<=m;r2++){
                                        if(str[l2][r2]=='#'||judge(x1,y1,l1,r1,l2,r2))break;
                                        ans++;
                                    }
                                }
                            }
                    }
                }
            }
        cout<<(ans/2)%P<<endl;
    }
}P30;

首先,显然在枚举的过程中,对于 L 的两条边的长度是不必要在循环内检验的,可以直接用O(n2)预处理出来。
定义 right[i][j] 表示从 (i,j) 点向右拓展能够拓展的长度, up[i][j] 表示从 (i,j) 点向上拓展能够拓展的长度。
因此我们枚举 L 的拐点坐标,会有以下三种情况:
情况
我们设第一个L的坐标为 (x1,y1) ,第二个为 (x2,y2)

  • 对于第一种,只有标红的线段是限定的,其余线段无关,因此
    tot=up[x1][y1]right[x1][y1]right[x2][y1]min(up[x2][y1],x2x11);
  • 对于第二种,所有线段都无限制,因此
    tot=up[x1][y1]up[x2][y2]right[x1][y1]right[x2][y2]
  • 对于第三种,第一个 L 向上的与第二个L向右的无关,而交点虚线部分的处理就比较麻烦了。我们可以分情况讨论:
    • 交点处给第一个 L tot=right[x1][y1]min(up[x2][y2],x2x11)
    • 交点处给第二个 L ,第二个L向上的边在红色段已经在前一种方案的统计中算过,因此: tot=(up[x2][y2]min(up[x2][y2],x2x11))min(right[x1][y1],y2y11)

于是就写出了 O(n4) 的代码,期望得分80分。

void Init(){
    for(int j=1;j<=m;j++)
        for(int i=1;i<=n;i++){
            if(str[i][j]=='#')continue;
            if(i==1||str[i-1][j]=='#')up[i][j]=0;
            else up[i][j]=up[i-1][j]+1;
        }
    for(int i=1;i<=n;i++)
        for(int j=m;j>=1;j--){
            if(str[i][j]=='#')continue;
            if(j==m||str[i][j+1]=='#')right[i][j]=0;
            else right[i][j]=right[i][j+1]+1;
        }
}
struct P80{
    ll calc(int x1,int y1,int x2,int y2){//y1<y2
        if(x1==x2)return 1LL*Up[x1][y1]*Up[x2][y2]*min(Right[x1][y1],y2-y1-1)*Right[x2][y2];
        if(x1>x2)return 1LL*Up[x1][y1]*Up[x2][y2]*Right[x1][y1]*Right[x2][y2];
        else{
            int t=min(Up[x2][y2],x2-x1-1);
            return 1LL*Up[x1][y1]*Right[x2][y2]*(1LL*Right[x1][y1]*t+1LL*(Up[x2][y2]-t)*min(Right[x1][y1],y2-y1-1));
        }
    }
    void solve(){
        ll ans=0;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                if(str[i][j]=='#')continue;
                for(int k=j+1;k<=m;k++)
                    if(str[i][k]=='#')break;
                    else Right[i][j]++;
            }
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                if(str[i][j]=='#')continue;
                for(int k=i-1;k>=1;k--)
                    if(str[k][j]=='#')break;
                    else Up[i][j]++;
            }
        for(int y1=1;y1<=m;y1++)
            for(int x1=1;x1<=n;x1++){
                if(str[x1][y1]=='#')continue;
                //y2==y1 judge
                for(int x2=x1+1;x2<=n;x2++){
                    if(str[x2][y1]=='#')continue;
                    ans+=1LL*Up[x1][y1]*Right[x1][y1]*Right[x2][y1]*min(Up[x2][y1],x2-x1-1);
                }
                for(int y2=y1+1;y2<=m;y2++)
                    for(int x2=1;x2<=n;x2++){
                        if(str[x2][y2]=='#')continue;
                        ans+=calc(x1,y1,x2,y2);
                    }
            }
        cout<<ans%P<<endl;
    }
}P80;

其实标程的复杂度就是 O(n4) ,然而 KyleYoung 大神写出了 O(n2) 的解法…Orz
凭借“正难则反”的指导思想,我们可以先算出总方案数,再减去不符合的方案数。
同样的,对于不符合的方案数,也是三种情况。我们可以选择一个特殊点来枚举,即图中两个 L 的交叉点。设此点坐标为(x,y)
首先我们与处理出 left 数组与 down 数组。 left[i][j] 表示 (i,j) 左边各点向上延伸的方案数之和, down[i][j] 表示 (i,j) 下边向右延伸的方案数之和。

left[i][j]=left[i][j1]+up[i][j1]
down[i][j]=down[i+1][j]+right[i+1][j]

情况
- 对于第一种,第一个 L 的右边范围为与第二个L的向上延伸范围为红色区域,其余线段无关: tot=(up[i][j]+1)down[i][j](right[i][j]+1)left[i][j]
- 对于第二种: tot=right[i][j]up[i][j]left[i][j](right[i][j]+1)
- 对于第三种: tot=right[i][j]up[i][j]down[i][j](up[i][j]+1)
于是就解决了这道题,复杂度 O(n2)

#include<stdio.h>
#define P 1000000007
#define M 1005
char str[M][M];
int up[M][M],down[M][M],left[M][M],right[M][M];
int n,m;
void Init(){
    for(int j=1;j<=m;j++)
        for(int i=1;i<=n;i++){
            if(str[i][j]=='#')continue;
            if(i==1||str[i-1][j]=='#')up[i][j]=0;
            else up[i][j]=up[i-1][j]+1;
        }
    for(int i=1;i<=n;i++)
        for(int j=m;j>=1;j--){
            if(str[i][j]=='#')continue;
            if(j==m||str[i][j+1]=='#')right[i][j]=0;
            else right[i][j]=right[i][j+1]+1;
        }
    for(int i=1;i<=n;i++)
        for(int j=2;j<=m;j++)
            if(str[i][j]!='#')left[i][j]=left[i][j-1]+up[i][j-1];
    for(int j=1;j<=m;j++)
        for(int i=n-1;i>=2;i--)
            if(str[i][j]!='#')down[i][j]=down[i+1][j]+right[i+1][j];
}
int total(){
    int sum=0,res=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(str[i][j]!='#'){
                int t=1LL*up[i][j]*right[i][j]%P;
                res=(res+1LL*t*sum)%P;
                sum=(sum+t)%P;
            }
    return res;
}
int calc1(){
    int res=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(str[i][j]!='#')res=(res+1LL*(up[i][j]+1)*down[i][j]%P*(right[i][j]+1)%P*left[i][j]%P)%P;
    return res;
}
int calc2(){
    int res=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)//横向 
            if(str[i][j]!='#')
                res=(res+1LL*right[i][j]*up[i][j]%P*left[i][j]%P*(right[i][j]+1)%P)%P;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)//竖直 
            if(str[i][j]!='#')res=(res+1LL*right[i][j]*up[i][j]%P*down[i][j]%P*(up[i][j]+1)%P)%P;
    return res;
}
int doit(int x){
    return (x%P+P)%P;
}
int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%s",str[i]+1);
    Init();
    printf("%d\n",doit(doit(total()-calc1())-calc2()));
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值