【解析】枚举找规律
- 由 1 , 2 , 4 , 8 1,2,4,8 1,2,4,8组成的 4 4 4位数的个数是 A 4 4 = 24 A_4^4=24 A44=24
- 由 1 , 1 , 2 , 4 1,1,2,4 1,1,2,4组成的 4 4 4位数有:
1124,1142,1214,1241,1412,1421,2114,2141,2411,4112,4121,4211
,共 12 12 12个- 由 1 , 1 , 2 , 8 1,1,2,8 1,1,2,8组成的 4 4 4位数的个数是 12 12 12个
- 由 1 , 1 , 4 , 8 1,1,4,8 1,1,4,8组成的 4 4 4位数的个数是 12 12 12个
- 由 8 , 8 , 1 , 2 8,8,1,2 8,8,1,2组成的 4 4 4位数的个数是 12 12 12个
- 由 8 , 8 , 1 , 4 8,8,1,4 8,8,1,4组成的 4 4 4位数的个数是 12 12 12个
- 由 8 , 8 , 2 , 4 8,8,2,4 8,8,2,4组成的 4 4 4位数的个数是 12 12 12个
- 由 1 , 1 , 8 , 8 1,1,8,8 1,1,8,8组成的 4 4 4位数有:
1188,1818,1881,8118,8181,8811
,共 6 6 6个
答案为: 24 + 12 × 6 + 6 = 24+12\times6+6= 24+12×6+6=102
。
【扩展】有重复数字的不重复全排列问题:
例如:1 1 2 4这三个数只有12种全排列
分别为:1124,1142,1214,1241,1412,1421,2114,2141,2411,4112,4121,4211
,共 12 12 12个。
设第 i i i个数有 a i a_i ai个
a n s = n ! a 1 ! a 2 ! a 3 ! ⋯ a n ! ans=\frac{n!}{a_1!a_2!a_3!⋯a_n!} ans=a1!a2!a3!⋯an!n!
【解析】非连通无向图在边数一定的情况下,求最少顶点个数,可以让 1 1 1个顶点与其它顶点不连通,剩下的顶点组成一个完全图时满足顶点个数最少。此时,完全图的顶点个数为 8 8 8个,加上剩下的 1 1 1个点,该图至少有
9
个顶点。
【解析】5位数车牌倒过来恰好还是原来的数字,那么中间位只能是 0 0 0、 1 1 1、 8 8 8。确定了车牌前 2 2 2位,后 2 2 2位可以通过找其颠倒过来的相同的数字即可,可选的数字有: 0 0 0、 1 1 1、 8 8 8、 6 6 6、 9 9 9。除此之外,还要保证车牌上的 5 5 5 位数能被 3 3 3 整除,数据规模较小,可以一一枚举。
- 中间数字是
0
的情况,00
、06
、60
、09
、90
、18
、81
、66
、69
、96
、99
,共 11 11 11种。- 中间数字是
1
的情况,01
、10
、16
、61
、19
、91
、88
,共7
种- 中间数字是
8
的情况,08
、80
、11
、68
、86
、89
、98
、共7
种。答案为: 11 + 7 + 7 = 11 + 7 + 7 = 11+7+7=
25
。
【解析】等比数列,最基本的特点就是数列从第二项开始,每一项与前一项的比值,都是一个定值。比如数列{1,2,4,8,16,……},后一项与前一项的比值都是 2,那么这就是一个等比数列。
等比数列的通项公式是: a n = a 1 × q n − 1 a_n=a_1\times\ q^{n-1} an=a1× qn−1
486 486 486的标准分解式 = 2 1 × 3 5 =2^1\times3^5 =21×35,由此可见公比为3
。
#include
using namespace std;
int n;
int a[100];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
int ans = 1;
for (int i = 1; i <= n; ++i) {
if (i > 1 && a[i] < a[i - 1]) //第12行
ans = i;
while (ans < n && a[i] >= a[ans + 1]) //第14行
++ans;
printf("%d", ans); //第16行
}
return 0;
}
第 16 16 16 行输出 ans
时,ans
的值一定大于 i
。错误
【解析】举相反的情况。当
a[]
是一个严格单调上升序列时,ans
始终等于i
。
若将第 12 12 12 行的 “<” 改为 “!=” 程序输出的结果不会改变。正确
【解析】算法是在数组
a[]
中找到i
位置及其右边的连续序列中最后一个小于等于a[i]
的数的位置。实现的关键在于while ()
循环中,所以第12行代码,对于最终结果没有影响。
当程序执行到第 16 16 16 行时,若 a n s − i > 2 ans - i > 2 ans−i>2,则 a [ i + 1 ] ≤ a [ i ] a[i+1] \le a[i] a[i+1]≤a[i]。正确
【解析】若 a n s − i > 2 ans - i > 2 ans−i>2,说明14行的
while()
循环至少执行了3次,那么a[i+1]
一定小于等于a[i]
。
若输入的 a
数组是一个严格单调递增的数列,此程序的时间复杂度是: O ( n ) O(n) O(n)。
【解析】 当
a
数组是一个严格单调递增的数列时,while()`循环一次都不执行,所以程序的时间复杂度是: O ( n ) O(n) O(n)。
最坏情况下,此程序的时间复杂度为: O ( n 2 ) O(n^2) O(n2)
【解析】 最坏情况下,当
a
数组是一个严格单调递减的数列时,while()
会执行 n − 1 , n − 2 , n − 3... n-1, n - 2, n - 3... n−1,n−2,n−3...,所以程序的时间复杂度是: O ( n 2 ) O(n^2) O(n2)。
#include
using namespace std;
const int maxn = 1000;
int n;
int fa[maxn], cnt[maxn];
int getRoot(int v) {
if (fa[v] == v) return v;
return getRoot(fa[v]);
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
fa[i] = i;
cnt[i] = 1;
}
int ans = 0;
for (int i = 0; i < n - 1; ++i) {
int a, b, x, y;
cin >> a >> b;
x = getRoot(a);
y = getRoot(b);
ans += cnt[x] * cnt[y]; //第25行
fa[x] = y;
cnt[y] += cnt[x];
}
cout << ans << endl;
return 0;
}
【解析】并查集实现集合集合合并。
若输入的 a a a 和 b b b 值均在 [ 0 , n − 1 ] [0, n-1] [0,n−1]的范围内,则对于任意 0 ≤ i < n 0 \le i错误
【解析】 c n t [ i ] cnt[i] cnt[i]表示集合中点的个数,若输入的 a a a 和 b b b 值均在 [ 0 , n − 1 ] [0, n-1] [0,n−1]的范围内, a a a 和 b b b 可能已经属于同一集合,若将同一个集合合并两次,那么
cnt[i]
中点的数量可能超过 n n n。
当 n n n 等于 50 50 50 时,若 a a a、 b b b 的值都在 [ 0 , 49 ] [0,49] [0,49] 的范围内,且在第 25 25 25 行时 x x x总是不等于 y y y,那么输出为 ( ):
【解析】第 25 25 25 行时 x x x总是不等于 y y y,保证了总是将两个不同的集合合并,那么合并过程如下图所示:
从下向上依次合并:
- 第一层, 50 50 50个 1 1 1两两相乘, a n s = 25 ans = 25 ans=25
- 第二层, 24 24 24个 2 2 2两两相乘, a n s = 25 + 12 × 4 = 73 ans = 25+12\times4=73 ans=25+12×4=73,合并之后,再将其中的一个 4 4 4和剩余的一个 2 2 2合并, a n s = 73 + 8 = 81 ans=73+8=81 ans=73+8=81
- 第三层, 10 10 10个 4 4 4两两相乘, a n s = 81 + 5 × 16 = 161 ans = 81+5\times16=161 ans=81+5×16=161,合并之后,再将剩余的一个 4 4 4和剩余的 6 6 6合并, a n s = 161 + 24 = 185 ans=161+24=185 ans=161+24=185
- 第四层, 4 4 4个 8 8 8两两相乘, a n s = 185 + 2 × 64 = 313 ans = 185+2\times64=313 ans=185+2×64=313,合并之后,再将
剩余的一个 8 8 8和 10 10 10合并, a n s = 313 + 80 = 393 ans = 313+80=393 ans=313+80=393- 第五层, 2 2 2个 16 16 16相乘, a n s = 393 + 16 × 16 = 649 ans = 393+16\times16=649 ans=393+16×16=649,合并之后,再将 32 32 32和 18 18 18合并, a n s = 649 + 32 × 18 = 1225 ans = 649+32\times18=1225 ans=649+32×18=1225
答案:
1225
此程序的时间复杂度是( O ( n 2 ) O(n^2) O(n2))。
【解析】未优化的并查集,在最坏情况下退化称为一个链表,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
#include
#include
using namespace std;
const int max1 = 202;
string s, t;
int pre[max1], suf[max1];
int main() {
cin >> s >> t;
int slen = s.length(), tlen= t.length();
for (int i = 0, j = 0; i < slen; ++i) {
if (j < tlen && s[i] == t[j]) ++j;
pre[i] = j;// t[0..j-1]是s[0..i]的子序列
}
for (int i = slen - 1, j = tlen - 1; i >= 0; --i) {
if(j >= 0 && s[i] == t[j]) --j; //第16行
suf[i]= j; //t[j+1..tlen-1]是s[i..slen-1]的子序列,第17行
}
suf[slen] = tlen -1;
int ans = 0;
for (int i = 0, j = 0, tmp= 0; i <= slen; ++i) {
while (j <= slen && tmp >= suf[j] + 1) ++j; //第22行
ans = max(ans, j - i - 1); //第23行
tmp = pre[i];
}
cout << ans << endl;
return 0;
}
提示:
t[0..pre[i]-1]
是s[0..i]
的子序列;
t[suf[i]+1..tlen-1]
是s[i..slen-1]
的子序列。
【解析】
pre[i]
表示s[0..i]
至多可以从前往后匹配到t
串的哪一个字符,此时t[0…pre[i]-1]是s[0…i]的子序列。suf[i]
用于记录s[i..slen-1]
至多从后到前匹配到t的哪一个字符,此时t[suf[i]+1..tlen-1]
是s[i..slen-1]
的子序列。本题是求s
中连续删除至多几个字母后,t
仍然是s
的子序列。
程序输出时,suf
数组满足:对任意 0 ≤ i < s l e n , s u f [ i ] ≤ s u f [ i + 1 ] 0 \le i正确
【解析】从16、17行的代码中可以看出,随着
i
不断减小,如果s[i] == t[j]
,j
会减小;否则j
不变,说明 s u f [ i ] ≤ s u f [ i + 1 ] suf[i]≤suf[i+1] suf[i]≤suf[i+1]
当 t 是 s 的子序列时,输出一定不为 0 0 0。错误
【解析】手动模拟,
s = "a", t = "a"
时,此时ans = 0
。
程序运行到第 23 23 23 行时,j - i - 1
一定不小于 0 0 0。错误
【解析】手动模拟,
s = "a", t = "b"
时,j - i - 1
可以为 − 1 -1 −1。
当 t 是 s 的子序列时,pre
数组和 suf
数组满足:对任意 0 ≤ i < s l e n 0\le i错误
【解析】由含义可知若
t
是s
子序列,t[0..pre[i]-1]
,t[sub[i+1]+1..lent-1]
是s[0..i]
,s[i+1..lens-1]
的子序列,不会重叠,即 p r e [ i ] − 1 < s u f [ i + 1 ] + 1 pre[i]-1< suf[i+1]+1 pre[i]−1<suf[i+1]+1,即 p r e [ i ] < = s u f [ i + 1 ] + 1 pre[i] <= suf[i+1]+1 pre[i]<=suf[i+1]+1。
若 tlen = 10
,输出为 0 0 0 ,则 slen
最小为:1
【解析】若
t
不是s
子串(或t==s
)输出都为0,但为保证程序执行,最少应输入一个字符。
若 tlen = 10
,输出为 2 2 2 ,则 slen
最小为:12
。
输出为 2 2 2说明
slen
最多连续删除 2 2 2个后为 10 10 10,所以最小为12
。
(匠人的自我修养)一个匠人决定要学习 n n n 个新技术,要想成功学习一个新技术,他不仅要拥有一定的经验值,而且还必须要先学会若干个相关的技术。学会一个新技术之后,他的经验值会增加一个对应的值。给定每个技术的学习条件和习得后获得的经验值,给定他已有的经验值,请问他最多能学会多少个新技术。
输入第一行有两个数,分别为新技术个数 n ( 1 ≤ n ≤ 1 0 3 ) n(1 \leq n \leq 10^3) n(1≤n≤103),以及已有经验值 ( ≤ 1 0 7 ) (\leq 10^7) (≤107)。
接下来 n n n 行。第 i i i 行的两个整数,分别表示学习第 i i i 个技术所需的最低经验值 ( ≤ 1 0 7 ) (\leq 10^7) (≤107),以及学会第 i i i 个技术后可获得的经验值 ( ≤ 1 0 4 \leq 10^4 ≤104)。
接下来 n n n 行。第 i i i 行的第一个数 m i ( 0 ≤ m i < n ) m_i(0 \leq m_i < n) mi(0≤mi<n),表示第 i i i 个技术的相关技术数量。紧跟着 m m m 个两两不同的数,表示第 i i i 个技术的相关技术编号,输出最多能学会的新技术个数。
下面的程序已 O ( n 2 ) O(n^2) O(n2)的时间复杂完成这个问题,试补全程序。
#include
using namespace std;
const int maxn = 1001;
int n;
int cnt[maxn];
int child [maxn][maxn];
int unlock[maxn];
int threshold[maxn],bonus[maxn];
int points;
bool find(){
int target=-1;
for (int i = 1;i<=n;++i)
if(① && ②){
target = i;
break;
}
if(target==-1)
return false;
unlock[target]=-1;
③
for (int i=0;i<cnt[target];++i)
④
return true;
}
int main(){
scanf("%d%d",&n, &points);
for (int i =1; i<=n;++i){
cnt [i]=0;
scanf("%d%d",&threshold[i],&bonus[i]);
}
for (int i=1;i<=n;++i){
int m;
scanf("%d",&m);
⑤
for (int j=0; j<m ;++j){
int fa;
scanf("%d", &fa);
child[fa][cnt[fa]]=i;
++cnt[fa];
}
}
int ans = 0;
while(find())
++ans;
printf("%d", ans);
return 0;
}
【解析】程序每次都先学习一个已经达到条件但还未学习的技能,学习后更新经验值和其他技能与该技能有关的学习条件,不断重复至没有技能可以学。
unlock
数组为对应技能需学习的前置技能数,大于 0 0 0说明有前置技能要学,为 − 1 -1 −1表示已学习。
- 空①,
unlock[i] == 0
表示对应技能需学习的前置技能数为 0 0 0,即第i
项技能解锁。- 空②,要学习第
i
项技能,除了第i
项技能已经解锁,还需要由足够的经验值,即points >= threshold[i]
- 空③,学习了第
i
项技能,将获得相应的经验值,即points += bonus[target]
。- 空④, 学习了第
i
项技能,还将解锁以第i
项技能为前置条件的其它技能,即unlock[child[target][i]] -= 1
- 空5,初始化第
i
项技能的前置技能的个数,即unlock[i] = m
2.(取石子)Alice 和 Bob 两个人在玩取石子游戏,他们制定了 n n n 条取石子的规则,第 i i i 条规则为:如果剩余的石子个数大于等于 a[i]
且大于等于 b[i]
,那么她们可以取走 b[i]
个石子。他们轮流取石子。如果轮到某个人取石子,而她们无法按照任何规则取走石子,那么他就输了,一开始石子有 m
个。请问先取石子的人是否有必胜的方法?
输入第一行有两个正整数,分别为规则个数 n ( 1 ≤ n ≤ 64 ) n(1 \leq n \leq 64) n(1≤n≤64),以及石子个数 m ( ≤ 1 0 7 ) m(\leq 10^7) m(≤107)。
接下来 n n n 行。第 i i i行有两个正整数 a [ i ] a[i] a[i] 和 b [ i ] b[i] b[i]。 1 ≤ a [ i ] ≤ 1 0 7 1 \leq a[i] \leq 10^7 1≤a[i]≤107, 1 ≤ b [ i ] ≤ 64 1 \leq b[i] \leq 64 1≤b[i]≤64
如果先取石子的人必胜,那么输出“Win”,否则输出“Loss”
提示:
可以使用动态规划解决这个问题。由于 b[i]
不超过 64 64 64,所以可以使用位无符号整数去压缩必要的状态。
status 是胜负状态的二进制压缩,trans 是状态转移的二进制压缩。
代码说明:
“~”表示二进制补码运算符,它将每个二进制位的 0 0 0 变成 1 1 1、 1 1 1 变为 0 0 0;
而“^”表示二进制异或运算符,它将两个参与运算的数重的每个对应的二进制位一一进行比较,若两个二进制位相同,则运算结果的对应二进制位为 0 0 0,反之为 1 1 1。
ull
标识符表示它前面的数字是 unsigned long long
类型。
试补全程序。
#include
#include
using namespace std ;
const int maxn = 64;
int n,m;
int a[maxn], b[maxn];
unsigned long long status, trans;
bool win;
int main() {
scanf("%d%d",&n, &m);
for (int i = 0; i < n; ++i)
scanf("%d%d", &a[i], &b[i]);
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
if (a[i] > a[j]) {
swap(a[i], a[j]);
swap(b[i], b[j]);
}
status = ①;
trans = 0;
for (int i = 1, j = 0; i <= m; ++i) {
while (j < n && ②) {
③;
++j;
}
win = ④;
⑤;
}
puts(win ? "Win" : "Loss");
return 0;
}
【解析】使用动态规划的思想解决问题:
- 状态表示:
f[i]
表示有i
个石子时,先手有无必胜的策略。- 状态转移:若对于
i
个石子有先手必赢策略,则存在存在规则j
( i ≥ a [ j ] , i ≥ b [ j ] ) (i \ge a[j], i \ge b[j]) (i≥a[j],i≥b[j]),使得有i - b[j]
个石子时,先手必败,即f[i - b[j]] = false
。那么状态转移方程:
f [ i ] = O R ( ! f [ i − b [ j ] ] ) , i ≥ a [ j ] 并 且 i ≥ b [ j ] f[i]=OR(!f[i - b[j]]), i \ge a[j] 并且i \ge b[j] f[i]=OR(!f[i−b[j]]),i≥a[j]并且i≥b[j]- 初始状态:
f[0] = 0
,表示 0 0 0个石子时,先手必败。题目给出的策略数和数组
b
数字都不超过64,所以仅考虑f[i-1]..f[i-64]
,可将其状态压缩至一个ull
整数中。其中status
用于记录对于i
个石子,i-1..i-64
是否有先手必胜策略。
- 空①,初始化
status
,最开始石子是0个,应该是先手必败的状态,所以最低位不能是1,因此可选status = ~0ull&1
。- 空②,题目实现有将规则按
a[i]
进行从小到大排序,所以可使用规则只增加不减少。此循环用于增加当前可选的规则,当石子数量i
达到规则a[j]
时,即可发生状态转移,使用该规则。状态转移到trans
变量,因此可选a[j] == i
。- 空③,此行是用来在原有规则上,新增”取
b[j]
个石子”的规则。二进制新增用|
。因此可选trans |= 1ull << (b[j] - 1)
- 空④,计算win的值,先手是否必胜。对当前状态和以前状态做判断。选择:
~status & trans
。
-空⑤,更新status
状态值,将当前win
值记录到status
中。status = status << 1 ^ win
。