题目列表
2928. 给小朋友们分糖果 I
2929. 给小朋友们分糖果 II
2930. 重新排列后包含指定子字符串的字符串数目
2931. 购买物品的最大开销
一、给小朋友们分糖果I
看一眼数据范围,如果没有啥其他想法思路就直接暴力,时间复杂度O(n^2)
思路:枚举前两个小朋友分得的合法糖果数,看第三个小朋友的糖果数是否符合条件
代码如下
class Solution {
public:
int distributeCandies(int n, int limit) {
int ans=0;
for(int i=0;i<=min(n,limit);i++){
for(int j=0;j<=min(n-i,limit);j++){
ans+=(n-i-j<=limit);
}
}
return ans;
}
};
二、给小朋友们分糖果II
和上一题一样的题目,数据范围变大,不能暴力,需要优化时间复杂度,如何优化???
上一题思路是枚举两个元素,来看第三个元素是否符合条件,但其实我们只要枚举一个元素,就能知道剩余的糖果如何分配,才能合法,因为剩余两个元素的和是固定的
举个例子:如果剩余的糖果数为10,limit=3,分配的方式为:3和7,4和6,5和5,6和4,7和3,一共7-3+1=5种,即我们只要能算出第二个元素或第三个元素的取值范围,就能得到合法的方案数
设left为剩余糖果的数量,总共有三种情况
1.left<=limit,即剩下的糖果随便怎么分都行,共有left+1种方案,注意0和left也是一种方案
2.left>limit*2,即剩下的糖果无论怎么分,都会有一个人的糖果数>limit,方案数为0
3.limit<left<=limit*2,即有合法的分配方案,但不能随意分配,有限制,其中一个人的糖果数范围是[left-limit,limit],共有limit-(left-limit)+1种方案数
代码如下
class Solution {
public:
int distributeCandies(int n, int limit) {
int ans=0;
for(int i=0;i<=min(n,limit);i++){
int left=n-i;
if(left<=limit)
ans+=left+1;
else if(limit*2>=left){
int x=left-limit;
ans+=limit-x+1;
}
}
return ans;
}
};
这题还有一个O(1)时间复杂度的算法,利用容斥原理来做,不了解的可以去了解一下
这题可以转换为将n个球放入3个不同的盒子中,盒子可以为空,且每个盒子最多装limit个球,问共有多少方案?非常经典的数学题,解题思路是合法方案数 = 总的方案数 - 不合法的方案数,其中总的方案数很容易求,不合法的方案数可以用容斥原理求,两个的时间复杂度都为O(1)
1.总的方案数的求解
"隔板法" :将n个球放成一排,在他们中间插入两个隔板,将球分为三组,有多少种排列方式,注意两个隔板可以放在一起,因为题目允许盒子为空,根据排列组合,共有C(n+2,2)种方法
可能有人对这个公式的由来不理解,其实我们也可以用乘法原理来求解,我们先将一个隔板插入,有n+1个位置可以选,再将第二个隔板插入,这时第一个隔板也被看做是球,则有n+2个位置可以选,而两个隔板的放置可能出现第一个隔板的位置和上一次第二个隔板的位置相同,第二个隔板的位置和上一次第一个隔板的位置相同的情况,所以总方案数要除以2,为(n+1)*(n+2)/2=C(n+2,2)
2.不合法的方案数---容斥原理
集合A---A盒中球的数量超出limit的方案数
集合B---B盒中球的数量超出limit的方案数
集合C---C盒中球的数量超过limit的方案数
根据容斥原理:不合法的方案数=A+B+C-A∩B-A∩B-A∩B+A∩B∩C
而A=B=C,A∩B=A∩C=B∩C
所以我们只要求三个数就行
A:提前在A盒中放入limit+1个球,让剩下的球随便放,方案数为C(n-limit-1+2,2)
A∩B:提前在A盒、B盒中放入limit+1个球,让剩下的球随便放,方案数为C(n-2*(limit-1)+2,2)
A∩B∩C:提前在A盒、B盒、C盒中放入limit+1个球,让剩下的球随便放,方案数为C(n-3*(limit-1)+2,2)
注意:上面三个的前提是还有剩余的球,如果剩余的球<=0,则方案数为0
代码如下
class Solution {
typedef long long LL;
public:
LL C2(LL n)//计算C(n,2)
{
if(n>1)
return n*(n-1)/2;
else
return 0;
}
long long distributeCandies(int n, int limit) {
return C2(n+2)-3*C2(n-(limit+1)+2)+3*C2(n-2*(limit+1)+2)-C2(n-3*(limit+1)+2);
}
};
三、重新排序后包含指定子字符串的字符串的数目
这题也能用容斥原理来做:(题目中是小写,下面用大写代替)
A集合:L没有出现的方案数 25^n
B集合:E没有出现或只出现一次的方案数 25^n+n*25^(n-1)
C集合:T没有出现的方案数 25^n
A∩B:L没有出现 并且 E没有出现或只出现一次的方案数 24^n+n*24^(n-1)
A∩C:L没有出现 并且 T没有出现的方案数 24^n
B∩C:T没有出现 并且 E没有出现或只出现一次的方案数 24^n+n*24^(n-1)
A∩B∩C:L没有出现 并且 T没有出现 并且 E没有出现或只出现一次的方案数 23^n+n*23^(n-1)
不合法方案数(容斥原理):A+B+C-A∩B-A∩B-A∩B+A∩B∩C
总的方案数:26^n
计算幂,要用到快速幂的算法,时间复杂度为O(logn)
代码如下
class Solution {
typedef long long LL;
const int MOD=1e9+7;
public:
//快速幂
LL POW(LL a,LL b)//a^b
{
int res=1;
while(b){
if(b&1) res=(res*a)%MOD;
a=(a*a)%MOD;
b>>=1;
}
return res;
}
int stringCount(int n) {
//公式可以化简如下
LL ans=POW(26,n)-((75+n)*POW(25,n-1)-(72+2*n)*POW(24,n-1)+(23+n)*POW(23,n-1));
return (ans%MOD+MOD)%MOD;
}
};
当然如果你没想到或者没学过容斥原理,这题还能用分组背包来求解,问题转化为:每次在'a'-'z'中选择一个字母,要求最后选择了至少1个L,1个T,2个E的方案数,时间复杂度为O(n)
代码如下
class Solution {
const int MOD=1e9+7;
public:
int stringCount(int n) {
long long memo[n+1][2][2][3];
memset(memo,-1,sizeof(memo));
function<int(int,int,int,int)>dfs=[&](int i,int L,int T,int E)->int{
if(i==0)
return L==0&&T==0&&E==0;
if(memo[i][L][T][E]!=-1) return memo[i][L][T][E];
long long res=0;
res+=dfs(i-1,0,T,E);//选择L
res+=dfs(i-1,L,0,E);//选择T
res+=dfs(i-1,L,T,max(E-1,0));//选择E
res+=(long long)dfs(i-1,L,T,E)*23;//选择其他的字母
return memo[i][L][T][E]=res%MOD;
};
return dfs(n,1,1,2);
}
};
四、购买物品的最大开销
这题反倒是没什么难点, 我们根据题意,就能猜到小天数*小的商品价格得到的乘积之和为最大开销(根据排序不等式可知),接下来,只要证明我们能在某天买到与之对应的小的商品价格,这很容易证明:因为我们每次选择都是在每一行剩余元素的最小值组成的集合中,即我们可以选到剩余所有数中的最小值。
代码如下
class Solution {
public:
long long maxSpending(vector<vector<int>>& values) {
int n=values.size(),m=values[0].size();
vector<int>dp(m*n);
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
dp[i*m+j]=values[i][j];
}
}
sort(dp.begin(),dp.end());
long long ans=0;
for(int i=0;i<m*n;i++){
ans=(ans+(long long)(i+1)*dp[i]);
}
return ans;
}
};