第22次CSP认证 第4题 校门外的树(3种方法,非常详细)(类dp+数学)

链接:
官网:
http://118.190.20.162/view.page?gpid=T125
Acwing:https://www.acwing.com/problem/content/description/3417/
在这里插入图片描述
题意:顺序给出数轴上N给不相等的点,记为序列a,首先将大区间[a[0],a[N-1]]划分成若干个子区间[li,ri),然后在子区间上面选取若干个点,要求这些点和区间端点构成等差数列。端点不能作为选取的点。问选取点的集合数量有多少。
分析:首先这是一个有限制的选择问题,我们见到的选择问题通常用dp、类dp、记忆化搜索来解决。dp和记忆化搜索的思想都是先把问题分为多个阶段的问题,每一个阶段都会对应一个状态,后面阶段的状态要利用已经求出的状态。
本题同理,首先求包含N个数的序列的方案数目这个问题划分阶段,先求包含1个数序列的方案数,然后求包含2个数的…以以此类推,最后求出包含N个数的,就是最后的答案。
于是本题第i个状态就是从第0个数第i个数的方案总数,即dp[i],一个维度就可以表示状态
之后要看怎么用之前的阶段求出各个阶段的状态值。对于第1个状态,就是第0个数到第1个数的约数个数,对于第i个状态,我们首先可以将第i个状态这个大集合划分成通过dp[i-1]求出的部分、通过dp[i-2]求出的部分…通过dp[0]求出的部分。比如在求通过第i-1个状态即dp[i-1]求出的部分时,由于第i-1之前的方案取法不会影响i-1到i的取法,所以从0到i-1的取法有dp[i-1]种,而i-1到i的取法有a[i]-a[i-1]的约数个数个,设为cnt个,对于dp[i]的贡献是cnt*dp[i-1]。
但是在求通过dp[i-2]求出的贡献值时,i-2到i的方案会不会和i-1到i的方案重合?注意,现在要将dp[i]划分成无交集,且并集为全集的子集,我们需要自己设计划分方式。我们的方向是,能写出简单的dp[i]与dp[0…i-1]关系的式子。而我们已经得到dp[i-1]的贡献是dp[i-1]*cnt,我们希望能找到0到i-2的cnt。

最后我们发现如果i-2的cnt设置成包含i-2到i的约数,同时不含i-1到i的约数的集合的元素个数,我们就可以将问题不重不漏的划分。同时题目说有障碍的地方不能种树,所以i-2到i的方案本身就不会包含i-1到i的方案使用过的约数,因为i-1到i的方案使用过的约数一定会经过端点。
暴力找约数的方法


#include <iostream>
#include<bits/stdc++.h>
typedef long long ll;
const int maxn=1000+5;
const int mod=1000000000+7;
const int maxa=100000+5;
using namespace std;
ll dp[maxn+5];
ll a[maxn+5];
vector<ll>v[maxa+10];
bool book[maxa+5];
ll solve(ll l,ll r){
    ll x=a[r]-a[l];
    ll cnt=0;
    if(x==1){//注意,相距为1不能种树,而且之后约数1也不能再次使用,所以book[1]=1
        book[1]=1;
        return 0;
    }
    if(book[1]==0){
        book[1]=1;
        cnt++;
        //cout<<1<<"*"<<endl;
    }
    for(ll i=2;i*i<=x;i++){
        if(x%i==0){
            if(book[i]==0){
                book[i]=1;cnt++;
            }
            if(book[x/i]==0){
                book[x/i]=1;cnt++;
            }
        }
    }
    book[x]=1;//注意,x不能被第j-1之前的利用
    return cnt;
}
int main()
{
    int N;
    cin>>N;
    for(int i=0;i<N;i++){
        scanf("%lld",&a[i]);
    }
    dp[0]=1;
    for(ll i=1;i<N;i++){
        memset(book,0,sizeof book);
        for(ll j=i-1;j>=0;j--){
            dp[i]=(dp[i]+dp[j]*solve(j,i))%mod;
        }
    }
    cout<<dp[N-1]<<endl;
    return 0;
}

在这里插入图片描述

打表找约数

#include <iostream>
#include<bits/stdc++.h>
typedef long long ll;
const int maxn=1000+5;
const int mod=1000000000+7;
const int maxa=100000+5;
using namespace std;
ll dp[maxn+5];
ll a[maxn+5];
vector<ll>v[maxa+10];
bool book[maxa+5];
int main()
{
    for(int i=1;i<maxa;i++){//对于所有i的倍数,i都是他们的约数
        for(int j=2*i;j<maxa;j+=i){//从二倍开始,可以排除约数为自己的情况,因为有障碍的地方不能种树
            v[j].push_back(i);
        }
    }
    int N;
    cin>>N;
    for(int i=0;i<N;i++){
        scanf("%lld",&a[i]);
    }
    dp[0]=1;
    for(ll i=1;i<N;i++){
        memset(book,0,sizeof book);//每次对dp[i-1]的划分操作对dp[i]的划分操作没有影响,要初始化标记数组
        for(ll j=i-1;j>=0;j--){
           ll x=a[i]-a[j];
           ll cnt=0;
           for(int i=0;i<v[x].size();i++){
                if(book[v[x][i]]==0){//不能在障碍上种树,对于第j-1个点a[j-1]来说,a[i]-a[j]的约数的倍数是端点,
                    book[v[x][i]]=1;//端点是障碍,所以j-1前的约数都不能重复用
                    cnt++;
                }
           }
           book[x]=1;//特别注意,虽然第j个端点不能cnt++,但是端点处不能种树,他影响第j-1个端点使得其不能利用约数a[i]-a[j]
           dp[i]=(dp[i]+dp[j]*cnt)%mod;
        }
    }
    cout<<dp[N-1]<<endl;
    return 0;
}

在这里插入图片描述
想试一试搜索,先是栈内爆空间,然后改了set超时。。注意,这时候book不能开全局了,因为每个状态的计算是会交替进行,而不是计算完一个再计算一个所以全局的book会乱套。

#include <iostream>
#include<bits/stdc++.h>
typedef long long ll;
const int maxn=1000+5;
const int mod=1000000000+7;
const int maxa=100000+5;
using namespace std;
ll dp[maxn+5];
ll a[maxn+5];
vector<ll>v[maxa+10];

ll N;
ll dfs(ll level){
    if(level>=N-1){
        return 1;
    }
//    bool book[maxa+5];
//    memset(book,0,sizeof book);//!!!!
    set<ll>book;
    for(ll i=level+1;i<N;i++){
        if(dp[i]==0){
            dp[i]=dfs(i);
        }
        ll cnt=0,x=a[i]-a[level];
        for(ll j=0;j<v[x].size();j++){
            if(find(book.begin(),book.end(),v[x][j])==book.end()){
                book.insert(v[x][j]);
                cnt++;
            }
        }
        book.insert(x);
        dp[level]=(dp[level]+dp[i]*cnt)%mod;
     //   cout<<"$"<<dp[level]<<" "<<dp[i]<<" "<<cnt<<endl;
    }
    return dp[level];
}
int main()
{
    for(int i=1;i<maxa;i++){//对于所有i的倍数,i都是他们的约数
        for(int j=2*i;j<maxa;j+=i){//从二倍开始,可以排除约数为自己的情况,因为有障碍的地方不能种树
            v[j].push_back(i);
        }
    }
    cin>>N;
    for(ll i=0;i<N;i++){
        scanf("%lld",&a[i]);
    }
    dp[0]=dfs(0);
    cout<<dp[0]<<endl;
    return 0;
}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值