AtCoder Beginner Contest 044 题解
A
题目分析
住的天数$N$小于限制天数$K$的时候,使用每晚价格 * 天数的常规算法。居住天数$N$大于$K$的时候,小于等于$K$的部分按照常规价格算,超过$K$的部分,按照另外一种价格算。
参考代码(C++)
#include<bits/stdc++.h> using namespace std; #define inf 0x3f3f3f3f #define N 520 #define mem(a,b) memset(a,b,sizeof(a)) #define F(i,j,r) for(int i=j;i<=r;++i) #define lowbit(x) (x&(-x)) typedef long long ll; //cin 、 cout 优化 void run(){ ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); } //快读 inline int read(){ int x=0,f=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-')f=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; } int main(){ run(); int n,k,x,y; cin >> n >> k >> x >> y; int ans = 0; if(n < k){ ans = n * x; }else{ ans = x * k + (n-k)*y; } cout << ans; return 0; }
B
题目分析
我自己的想法是用数组模拟的哈希表去统计每个字母出现的次数,最后扫描一遍,标记是否出现了个数为奇数的。
参考代码(C++)
#include<bits/stdc++.h> using namespace std; #define inf 0x3f3f3f3f #define N 110 #define mem(a,b) memset(a,b,sizeof(a)) #define F(i,j,r) for(int i=j;i<=r;++i) #define lowbit(x) (x&(-x)) typedef long long ll; int t,n; int h[N]; //cin 、 cout 优化 void run(){ ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); } //快读 inline int read(){ int x=0,f=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-')f=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; } int main(){ run(); char str[N]; cin >> str; int len = strlen(str); for(int i = 0; i < len;i++){ h[str[i]-'a'] ++; } bool isodd = false; //标记是否出现奇数个数的字母 for(int i = 0; i < 26;i++){ if(h[i] % 2 == 1){ isodd = true; }else continue; } if(isodd){ puts("No"); }else{ puts("Yes"); } return 0; }
C
题目描述
题目翻译的原则是删繁从简,那么这个题的大意,是这种的:给咱们$N$张卡片,每个卡片有一个权值$X_i$,然后咱们选任意多张卡片(不能为0),使得这些卡片权值的平均值严格等于$A$,问你有多少种选法。
题目分析
其实想打暴力的,每张卡片有选择和不选择两种情况,那么差不多就是$2^N$的时间复杂度,$N$ = 50,直接喜提$TLE$。开VP的时候,我自己确实是只有这个题没有想起来,除了了暴力,都忘记可以用什么知识点了😳。
赛后搜dalao们写的题解,看到标题上写 动态规划 的时候,我突然悟了🤤。因为动态规划是可以理解是对暴力的优化,优化的就是那些重复的,无用的状态、步骤。
本题要咱们找平均数是$A$的选法,根据闫式DP分析的思路,我们集合维护的属性是数量(Count),至于集合的定义,可以开一个三维数组(可以优化到二维的),$f[i][j][k]$表示在前$i$张中卡片中选择,选择的数量是$j$,然后总和是k的方案的数量。
回忆我上面所说,当前这张卡片有选择和不选择两种情况,那么对应到状态转移方程中,想要从$i-1$的状态转移迈向$i$的状态,关键要讨论的,就是这个第$i$个物品,选择还是不选择呢,综合两种情况,就可以得到咱们的状态转移方程
$$f[i][j][k] = f[i-1][j][k](选当前的这个卡片)+ f[i-1][j-1][k-x[i]](不选当前这个物品)$$
参考代码(C++)
#include<bits/stdc++.h> using namespace std; #define inf 0x3f3f3f3f #define N 55 #define mem(a,b) memset(a,b,sizeof(a)) #define F(i,j,r) for(int i=j;i<=r;++i) #define lowbit(x) (x&(-x)) typedef long long ll; ll x[N]; ll f[N][N][N*N]; //cin 、 cout 优化 void run(){ ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); } //快读 inline int read(){ int x=0,f=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-')f=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; } int main(){ run(); int n,a; cin >> n >> a; //待会从1开始枚举从来避免i-1出现负数的情况。在开始动态规划之前初始化 for(int i = 1; i <= n;i++) cin >> x[i]; f[0][0][0] = 1;//题目说的确实至少选一个,但是 0 0 0 确实一种结果。至于其他的j = 0的情况,其结果都是0 for(int i = 1; i <= n;i++){ for(int j = 0; j <= i;j++){ //这里写0是需要f[0][0][0] = 1这种情况作为启动。 for(int k = 2500; k >=0;k--){ f[i][j][k] += f[i-1][j][k];//这里取 = 或者 取 += 都是可以Ac 的。+= 符合逻辑,直接= 是间接符合逻辑 if(k >= x[i] && j >= 1) f[i][j][k] += f[i-1][j-1][k-x[i]]; } } } // for(int i = 1;i <= n;i++){ // for(int j = 0; j <= 2500;j++) // printf("f[%d][0][%d] = %d\n",i,j,f[i][0][j]); // } //对于统计数量类型的动态规划,最后都是需要扫一遍,统计最终结果的 ll ans = 0; for(int j = 1; j <= n;j++){ ans += f[n][j][a*j]; } cout << ans <<'\n'; return 0; }
当然,这个代码可以有两种优化(动态规划能优化的几乎只有空间),其一是滚动数组优化空间,其二是降到二维数组。
滚动数组
空间上优化一半吧
#include<bits/stdc++.h> using namespace std; #define inf 0x3f3f3f3f #define N 55 #define mem(a,b) memset(a,b,sizeof(a)) #define F(i,j,r) for(int i=j;i<=r;++i) #define lowbit(x) (x&(-x)) typedef long long ll; ll f[2][N][N*N]; int x[N]; //cin 、 cout 优化 void run(){ ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); } //快读 inline int read(){ int x=0,f=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-')f=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; } int main(){ run(); int n,a; cin >> n >> a; for(int i = 1;i <= n;i++) cin >> x[i]; f[0][0][0] = 1; f[1][0][0] = 1; //^异或运算又被称为不进位加法 1 ^ 0 = 1, 1 ^ 1 = 0,以此实现0和1之间的滚动 for(int i = 1,roll = 1; i <= n;i++,roll ^= 1){ for(int j = 0; j <= n;j++){ for(int k = 2500;k >= 0; k--){ f[roll][j][k] = f[roll ^ 1][j][k]; if(k >= x[i] && j >= 1) f[roll][j][k] += f[roll^1][j-1][k-x[i]]; } } } ll ans = 0; //n & 1 可以区分奇数1和偶数0,从而区分空间 for(int j = 1,roll = n&1; j <= n;j++) ans += f[roll][j][j*a]; cout << ans; return 0; }
二维数组
至于打二维数组是看到这位博主的文章,我暂时不能很好get,后面回来再尝试理解了。
要点总结
这个题的状态转移方程这个就是普通的线性DP,没有什么代表性了,可以试着玩玩滚动数组,以及琢磨一下优化到二维。
D
题目描述
D题属于是,思维题,思维题我也没有什么折,模拟出来,只是过了一半的测试数据。
题目意思大概是这种的:
给咱们一个整数$n$和整数$s$,这个整数$s$呢,是整数$n$和整数$b$通过某种运算$f$得到的结果。关于这个$f$运算法则,是这种定于的:假如$b$大于$n$,那么这个结果$s$ = $n$。假如$n$大于等于$b$,最后的结果是整数$n$不断被整数$b$取模之后结果的和。现在给咱们$n$ 和 $s$ ,让咱们找符合条件的,最小的$b$,假如找不到,就输出-1.
题目分析
这个题了,都可能知道,得分类讨论了,
情况一:$s$ == $n$的时候,最小的整数$b$是$n + 1$;
情况二:$s$ > $n$的时候,是无解,得输出-1,
那么,这个题比较考思维的情况三来了,当$s$ < $n$的时候,该咋办呢
我看dalao们是酱紫理解的。
他们的思考方式,让我回忆起了Y总说的话。数据范围是出题人给我们的提示
对于这个题而言,数据范围是$10^{11}$,即使朴素的一重循环,$O(N)$的时间复杂度做法来解决这个题了,都是没法全部Ac的。$O(N)$过不了,但是$O(\sqrt{N})$的做法,是一定可以过的,好了,情况三的核心命脉拿捏到了。
我们可以将本题的题面在情况三中抽象成数学等式:
\begin{equation}
\begin{cases}
s=x_0 + x_1 + x_2 + ... + x_m& \\
n= x_0 * b^0 + x_1*b^1+x_2*b^2 + ... + x_m*b^m&
\end{cases}
\end{equation}
//碎碎念:实践证明,latex写大括号公式的时,&符号前面不能有空格
根据上面的想法,咱们就着重瞄着这个能够出现根号的$\sqrt{}$的$b^2$展开毒打。
当最高次幂为2的时候,有:$b^2 <= n$,当最高次幂是3,是3的时候,仍然是满足 $b$ <= $\sqrt{n}$的,
那么,当最高次幂为2次幂及其以上的时候,可以直接枚举b,找到合适的解;
但是当 $b>\sqrt{n}$的时候,此时的数学公式是:
\begin{equation}
\begin{cases}
s=x_0 + x_1& \\
n= x_0 * b^0 + x_1*b^1&
\end{cases}
\end{equation}
经过化简,能够得到的式子是$(b-1)*x_1 = n - s$。那咱们在$(\sqrt{n-s})$的范围内枚举能使$(n-s)$的结果整除的$x_1$就可以找到相应的$b$了。
参考代码(C++)
#include<bits/stdc++.h> using namespace std; #define inf 0x3f3f3f3f #define N 520 #define mem(a,b) memset(a,b,sizeof(a)) #define F(i,j,r) for(int i=j;i<=r;++i) #define lowbit(x) (x&(-x)) typedef long long ll; ll n,s; //cin 、 cout 优化 void run(){ ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); } //快读 inline int read(){ int x=0,f=1; char c=getchar(); while(c<'0'||c>'9'){ if(c=='-')f=-1; c=getchar(); } while(c>='0'&&c<='9'){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; } int main(){ run(); cin >> n >> s; if(n == s){ cout << n + 1 <<'\n'; return 0; }else if(n < s){ cout <<-1 <<'\n'; return 0; }else{ // n > s //先求一个根号n ll mid = sqrt(n) + 1; bool isFind = false; //讨论两种情况 b <= mid.,枚举b到合适的值 for(ll b = 2; b <= mid;b++){ //注意这个题数据很大,不要轻易用int ll sum = 0; ll tmp = n; while(tmp){ sum += tmp%b; tmp /= b; } if(sum == s){ cout << b <<'\n'; isFind = true; break; } } //讨论b > mid的情况 if(!isFind){ ll num = n - s; mid = sqrt(num)+1; ll ans ; for(int x1 = mid; x1 >= 1;x1--){ if(num % x1 == 0){ ans = (num/x1) + 1; ll x0 = s - x1; //判断得到的结果是否合法 //这儿的x1,x0都是ans(即b)取模后得到的,肯定是小于b的 if(ans >= 2 && ans > x1 && x0 >= 0 && ans > x0 ){ cout << ans <<'\n'; isFind = true; break; } } } } //情况三的两种讨论都尝试了还是没有找到,那确实是找不到合适的了 if(!isFind){ cout <<-1; return 0; } } return 0; }
要点总结
需要学会这里根据数据范围定算法的思维,至于这种情况三这里根据$\sqrt{N}$算法而生的讨论方式,希望能记住吧,原来还可以这种玩🤔🤔🤔