• Luogu1185 | 大师(线性DP)


    题目背景

    建筑大师最近在跟着数学大师ljt12138学数学,今天他学了等差数列,ljt12138 决定给他留一道练习题。

    题目描述

    ljt12138 首先建了 (n) 个特斯拉电磁塔,这些电塔排成一排,从左到右依次标号为 (1)(n) ,第 (i) 个电塔的高度为 (h[i])

    建筑大师需要从中选出一些电塔,然后这些电塔就会缩到地下去。这时候,如果留在地上的电塔的高度,从左向右构成了一个等差数列,那么这个选择方案就会被认为是美观的。

    建筑大师需要求出,一共有多少种美观的选择方案,答案模 (998244353)

    注意,如果地上只留了一个或者两个电塔,那么这种方案也是美观的。地上没有电塔的方案被认为是不美观的。

    输入格式

    第一行一个正整数 (n)

    第二行 (n) 个非负整数,第 (i) 个整数是第 (i) 个电塔的高度 (h[i])

    输出格式

    输出一个整数,表示美观的方案数模 (998244353) 的值。

    输入输出样例

    输入 #1

    8
    13 14 6 20 27 34 34 41

    输出 #1

    50

    输入 #2

    100
    90 1004 171 99 1835 108 81 117 141 126 135 144 81 153 193 81 962 162 1493 171 1780 864 297 180 532 1781 189 1059 198 333 1593 824 207 1877 216 270 225 1131 336 1875 362 234 81 288 1550 243 463 1755 252 406 261 270 279 288 1393 261 1263 297 135 333 872 234 881 180 198 81 225 306 180 90 315 81 81 198 252 81 297 1336 1140 1238 81 198 297 661 81 1372 469 1132 81 126 324 333 342 81 351 481 279 1770 1225 549

    输出 #2

    11153

    说明/提示

    我们用 (v) 表示最高的电塔高度。

    对于前 (30\%) 的数据,(n<=20)

    对于前 (60\%) 的数据,(n<=100,v<=2000)

    对于另外 (20\%) 的数据,所有电塔的高度构成一个等差数列。

    对于 (100\%) 的数据,(n<=1000,v<=20000)

    ——————————————————————————————————

    文字部分来自 @stdcall__

    相应代码为博主本人所写

    30 pts

    (n le 20)

    暴力分。

    首先你要读懂题,题目的意思是,给你一个数组,求出有多少个非空子序列是等差数列。

    所以可以用 (O(2^n)) 枚举一个子序列,再用 (O(n)) 判断她是不是等差数列。

    总复杂度 (O(2^n * n))

    代码如下:

    #include <bits/stdc++.h>
    #define MAXN 1007
    #define mod 998244353
    using namespace std;
    int n,ans,h[MAXN];
    vector<int> seq;
    inline bool check() {
    	if ((int)seq.size()==1) return true;
    	int d=seq[1]-seq[0];
    	for (int i=2;i<(int)seq.size();i++) 
    		if (seq[i]-seq[i-1]!=d) return false;
    	return true;
    }
    int main() {
    	scanf("%d",&n);
    	for (int i=1;i<=n;i++) scanf("%d",&h[i]);
    	int num=(1<<n)-1;
    	for (int i=num;i;i=(i-1)&num) { //枚举子集的模板
    		seq.clear();
    		int cnt=0;
    		for (int j=i;j;j>>=1) {
    			cnt++;
    			if (j&1) seq.push_back(h[cnt]);
    		}
    		if (check()) ans++;
    	}
    	printf("%d",ans%mod);
    }
    
    

    60 pts

    (n le 100, v le 2000)

    从这里开始就需要 DP 了。

    我们观察到值域很小,这提示我们把和值域相关的量记入状态表示。

    (f(i,j)) 表示以位置i结尾,公差为j的等差数列有多少个。转移的时候,枚举一个小于 (i)(k) ,满足(h_k = h_i - j),然后从 (f(k,j)) 转移到 (f(i,j))

    注意,公差可以是负数,因此这里的j也可以是负数。

    转移的时候要小心,不要重复或者遗漏某些情况。

    复杂度 (O(n^2k))

    80 pts

    (n le 1000, v le 20000)

    整个数组是一个等差数列。

    在公差不为 (0) 的时候,任何几个相邻相隔 (k) 个位置的数字都可以组成一个等差数列。

    这时候,我们只要枚举首项的位置和 (k) ,就可以很轻松地计算出答案。

    注意,在公差等于 (0) ,也就是所有数字都相等的时候,答案是 (2^n-1),这一点需要特判,否则你会WA掉第13个点。

    代码如下:

    #include <bits/stdc++.h>
    #define MAXV 20007
    #define p 20000
    #define MAXN 1007
    #define mod 998244353
    using namespace std;
    //f[i][j] 表示以 i 结尾,公差为 j-p 的等差数列数量 
    int n,ans=0,h[MAXN];
    int f[MAXN][MAXV*2];
    inline int qpow(int a,int b) { //快速幂取模 
    	long long base=a,ans=1;
    	while (b) {
    		if (b&1) ans=ans*base%mod;
    		base=base*base%mod;
    		b>>=1;
    	}
    	return ans;
    } 
    int main() { 
    	memset(f,0,sizeof(f));
    	scanf("%d",&n);
    	for (int i=1;i<=n;i++) scanf("%d",&h[i]);
    	if (n>100) {
    		if (h[1]==h[2]) { //公差为 0 时,可以不按固定间隔选,特判z=之 
    			printf("%d",qpow(2,n)-1);
    			return 0;
    		}
    		for (int i=1;i<=n;i++) //枚举起点 
    			for (int j=1;j<n;j++) //枚举间隔 
    				ans=(ans+(n-i)/j)%mod;
    		printf("%d",ans+n);
    		return 0;
    	}
            //O(n*n*v) 的 DP
    	for (int i=1;i<=n;i++) {
    		for (int j=1;j<MAXV*2;j++) {
    			for (int k=1;k<i;k++)
    				if (h[k]+j-p==h[i])
    					f[i][j]=(f[i][j]+f[k][j]+1)%mod;
    			ans=(ans+f[i][j])%mod; 
    		}
    	}
    	printf("%d",(ans+n)%mod);
    	return 0;
    } 
    

    100 pts

    (n le 1000, v le 20000)

    考虑在 60 分的算法基础上继续优化。

    我们可以把这个算法简化为,枚举一个公差 (d),然后统计有多少个公差为 (d) 的等差数列。

    枚举公差的时间复杂度是 (O(v)) ,观察数据范围可以猜测,统计的时间复杂度 (O(n)),总复杂度是 (O(nv))

    我们考虑60分的那个 (O(n^2)) 的DP,用到这个统计上来,用 (f(i)) 表示以i结尾的,公差为d的等差数列有多少个,转移的时候枚举一个小于 (i)(k),然后当 (h_k = h_i - d) 的时候从 (f(k)) 转移到 (f(i))

    状态已经不可能再简化了,但是转移可以。

    我们发现,转移相当于一个求和,对小于 (i) 的所有高度等于 (h_i - d) 的位置的 DP 值求和。

    我们可以维护一个数组 (g) 来记录这个和,这样转移就只有两行了。

    (f(i) = g(h_i - d))

    (g(h_i) = g(h_i) + f(i))

    总复杂度是 (O(nv))

    代码如下:

    #include <bits/stdc++.h>
    #define MAXV 20007
    #define MAXN 1007
    #define mod 998244353
    using namespace std;
    //f[i][j] 表示以 i 结尾,公差为 j-p 的等差数列数量 
    //g[h[i]][j] 表示此前所有高度为 h[i] 的 f[i][j] 之和 
    int n,ans=0,h[MAXN];
    int f[MAXN],g[MAXV]; //使用了滚动数组滚掉了第二维
    int main() { 
    	memset(f,0,sizeof(f));
    	memset(g,0,sizeof(g));
    	scanf("%d",&n);
    	for (int i=1;i<=n;i++) scanf("%d",&h[i]);
            //滚动数组所以要交换循环次序
    	for (int j=-MAXV;j<MAXV;j++) {
    		memset(f,0,sizeof(f));
    		for (int i=1;i<=n;i++) g[h[i]]=0;
    		for (int i=1;i<=n;i++) {
    			f[i]=1;
    			if (h[i]-j>=0 && h[i]-j<MAXV) //范围是否合法
    				f[i]=(f[i]+g[h[i]-j])%mod;
    			g[h[i]]=(g[h[i]]+f[i])%mod;
    			ans=(ans+f[i])%mod;
    			
    		}
    		ans=(ans-n+mod)%mod; //减去每次多加的
    	}
    	printf("%d",(ans+n)%mod);
    	return 0;
    } 
    
  • 相关阅读:
    APPIUM Android 定位方式
    SQL Server 根据存储过程的结果集创建临时表
    Ubuntu18.04 设置开机自启动服务
    ubuntu-18.04 (各版本镜像下载) 及的环境初始化配置
    CentOS 7 编译安装PHP5.6.31
    Centos7 编译安装 MySQL 5.5.62
    Windows 2008 R2 远程桌面连接记录(客户端IP)
    CentOS crontab定时任务
    CentOS 7 安装MySql 5.5.60
    SQL Server 数据库错误码解释
  • 原文地址:https://www.cnblogs.com/zhwer/p/13230726.html
Copyright © 2020-2023  润新知