输入两个整数数组a,b
,找到两个数组的最短公共子序列c
。子序列的概念:如果c
是a
的子序列,则满足a
通过删除若干(也可以是0)个元素可以得到c
。
length(a), length(b) <= 1000; case <= 1000, 1 <= a[i], b[i] <= 1000
既然是找到最短的公共子序列,那么只要找到在一个a, b
中都出现过的元素即可。这里我们先读入数组a
,并使用无序堆unordered_set
来存储,然后再查找b
中的元素是否能在s
中找到。
[unordered_set
定义在
头文件中,查找操作时间复杂度是 O ( 1 ) O(1) O(1),而set
是有序堆,查找操作时间复杂度是 O ( l o g n ) O(logn) O(logn),类似的还有unordered_map, map
]
int main(int argc, char * argv[])
{
int T;
cin >> T;
while (T--) {
int n, m;
cin >> n >> m;
unordered_set<int> s;
int a, b, v = 0;
for (int i = 0; i < n; ++i) {
cin >> a;
s.insert(a);
}
for (int i = 0; i < m; ++i) {
cin >> b;
if (s.count(b)) v = b;
}
if (v) cout << "YES\n" << 1 << " " << v << endl;
else cout << "NO\n";
}
return 0;
}
有n
堆石头,第i
堆有a[i]
个石头,两个人玩游戏,每个人轮流从第一个非空石头堆中拿石头,最后没有石子可以拿的人输掉游戏,如果每个人都采取最优的决策,请判断先手的人是否能赢得游戏。
n <= 1e5; case <= 1000, 1 <= a[i] <= 1e9
先假设A先手,B后手。首先如果只有一堆石子,那么A赢。如果有两堆石子,且a[0] > 1
,那么总是A赢得游戏,因为先手的人总可以先拿走a[0] - 1
个石子,只留下一个石子,然后B拿走剩余的一个石子,然后A拿走最后一堆所有的石子,B没有石子可拿,A赢下游戏。那么我们就可以从后往前推,判断从当前这堆石子出发,A是否可以赢得游戏。由于只有一堆石子的情况下A可以赢下游戏,所以要从倒数第二堆石子开始递推。当f=true
时表示A赢下游戏,否则B赢下游戏。分两种情况讨论:
a[i] == 1
时,那么在剩下的游戏中,A就变成了后手,B变成了先手,A,B胜负互换,所以这里f = !f
;a[i] > 1
那么A总是可以赢下游戏的,因为如果想要在接下来的游戏中交换先后手那么可以直接拿光这堆石头,如果不想交换先后手,则拿a[i] - 1
个石头。const int N = 1e5 + 5;
int a[N];
int main(int argc, char * argv[])
{
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
for (int i = 0; i < n; ++i) cin >> a[i];
bool f = true;
for (int i = n - 2; i >= 0; --i) {
if (a[i] == 1) f = !f;
else {
if (!f) f = true;
}
}
if (f) cout << "First\n";
else cout << "Second\n";
}
return 0;
}
给两个二进制字符串(只包含字符0和1)a, b
长度为n
,现在可以对字符串进行如下操作:对字符串选择任意长度的前缀,先进行字符反转(0变成1,1变成0),然后再将该前缀子字符串进行翻转,例如:s=001011
,当选取前缀长度为3
时,先反转字符得到110011
,然后翻转得到011011
。现在任务是将字符串通过这种操作a
变成b
,最多只有3n
步,输出总共的步数以及每一步操作的前缀长度。
n <= 1000; case <= 1000
可以枚举a[i], 0 <= i < n
变为b[i]
,但是需要保证每一次操作不会影响到之前已经转变成功的字符,由于总的步数需要限制在3n
所以转变一个字符需要在3
步内,怎么保证在转变a[i]
的时候a[0],...a[i - 1]
都不发生变化呢?这里提供一个简单的思路,就是当a[i] != b[i]
,先对前缀长度为i
的字符串操作,然和对前缀长度为1
的字符串操作,然后再对前缀长度为i
的字符串操作,这里可以看出,字符a[i]
反转了3次,而a[0] - a[i - 1]
反转了两次,相当于不变。
int main(int argc, char * argv[])
{
int T;
cin >> T;
string a, b;
vector<int> ops;
while (T--) {
int n;
cin >> n;
cin >> a >> b;
ops.clear();
for (int i = 0; i < n; ++i) {
if (a[i] != b[i]) {
ops.push_back(i + 1);
ops.push_back(1);
ops.push_back(i + 1);
}
}
cout << ops.size();
for (auto & v : ops) cout << " " << v;
cout << endl;
}
return 0;
}
给两个二进制字符串(只包含字符0和1)a, b
长度为n
,现在可以对字符串进行如下操作:对于字符串选择任意长度的前缀,先进行字符反转(0变成1,1变成0),然后再将该前缀子字符串进行翻转,例如:s=001011
,当选取前缀长度为3
时,先反转字符得到110011
,然后翻转得到011011
。现在任务是将字符串通过这种操作a
变成b
,最多只有2n
步,输出总共的步数以及每一步操作的前缀长度。
n <= 1e5; case <= 1000
C1的困难版,这里官方Tutorial给出了两种方法。这里介绍一下较为巧妙的思路。先把字符串a
变为全0的字符串,再把b
变为全0的字符串,同时把关于b
的操作翻转接到a
的操作上,就可以实现将a
变为b
。那如何实现在n
步内将a
变为全0的字符串呢?只需要逐步将前缀变为全1或者全0的字符串即可,例如:0101->1101->0001->1111->0000
。
[这里为了保证a, b
转化为全0字符串,在a, b
后面添加了一个字符0,简化代码;rbegin(), rend()
可以实现对容器进行逆序遍历]
int main(int argc, char * argv[])
{
int T;
cin >> T;
string a, b;
int n;
while (T--) {
cin >> n;
cin >> a >> b;
a.push_back('0');
b.push_back('0');
vector<int> aops, bops;
for (int k = 1; k <= n; ++k) {
if (a[k] != a[k - 1]) aops.push_back(k);
if (b[k] != b[k - 1]) bops.push_back(k);
}
int len = aops.size() + bops.size();
cout << len;
for (auto it = aops.begin(); it != aops.end(); ++it) cout << " " << *it;
for (auto it = bops.rbegin(); it != bops.rend(); ++it) cout << " " << *it;
cout << endl;
}
return 0;
}
先解释一下归并的操作对于序列a, b
。
m e r g e ( a , b ) = [ a 1 ] + m e r g e ( [ a 2 , … , a n ] , b ) , a 1 < b 1 merge(a,b)=[a_1]+merge([a_2,…,a_n],b), a_1 < b_1 merge(a,b)=[a1]+merge([a2,…,an],b),a1<b1
m e r g e ( a , b ) = [ b 1 ] + m e r g e ( [ b 2 , … , b m ] , a ) , a 1 > b 1 merge(a,b)=[b_1]+merge([b_2,…,b_m],a), a_1 > b_1 merge(a,b)=[b1]+merge([b2,…,bm],a),a1>b1
这个操作是个迭代操作直至a
或者b
为空,即合成一个数组。
给一个长度为2n
置换数组,判断该数组是否可以由两个长度为n
的数组归并合成。
置换数组的概念:对于长度为n
的数组,数组元素只包含1, 2, ...n
,顺序可以是任意的,比如[4, 2, 3, 1, 5]
就是置换数组,但是[1, 2, 2]
不是。
n <= 2000; case <= 1000
熟悉归并排序的同学都很清楚,这就是归并排序的逆过程,这里给出归并排序的代码。
void merge(vector<int>& arr, int L, int R) {
if (L == R) return 0;
int M = (L + R) >> 1;
merge(arr, L, M);
merge(arr, M + 1, R);
vector<int> list;
int Lind = L, Rind = M + 1;
while (Lind <= M && Rind <= R) {
if (arr[Lind] <= arr[Rind]) list.push_back(arr[Lind++]);
else list.push_back(arr[Rind++]);
}
while (Lind <= M) list.push_back(arr[Lind++]);
while (Rind <= R) list.push_back(arr[Rind++]);
for (int k = L; k <= M; ++k) arr[k] = list[k - L];
}
可以看到后面两个循环是为了保证左右子数组都遍历完,那么后面两个循环有且只有一个起作用,这保证该循环添加到新数组list
后面的一段元素必定是某一个子数组的后缀,要么是左子数组的,要么是右子数组的。这个后缀有个明显的特征就是第一个元素是最大的,然后我们在list
中删除这个后缀。得到一个新数组,然后问题又变成了原来的问题,新数组是否可以通过两个子数组merge
得到,又可以进行上面的操作,如此迭代下去,就可以得到一系列的子数组。例如:[2, 3, 1, 4]->{[2, 3, 1], [4]}->{[2], [3, 1], [4]}
.
这样问题就转化为这些子数组是否可以组成两个长度为n
的数组,因为总长度是2*n
,所以可以转化为是否可以找到若干子数组组成一个长度为n
的数组,所以这就转化为了0-1背包问题,最简单的可以写成 O ( n 2 ) O(n^2) O(n2),这也完全可以AC,这里提供一个官方的 O ( n n ) O(n\sqrt n) O(nn)写法(为啥是 O ( n n ) O(n\sqrt n) O(nn)我也不知道)。
const int N = 4e3 + 5;
int dp[N];
bool vis[N];
int main(int argc, char * argv[])
{
int T, n;
cin >> T;
while (T--) {
memset(vis, false, sizeof(vis)); vis[0] = true;
cin >> n;
vector<int> ind;
int maxE = 0, pk;
for (int k = 0; k < 2*n; ++k) {
cin >> pk;
if (pk > maxE) {
maxE = pk;
ind.push_back(k);
}
}
ind.push_back(2*n);
vector<int> blens; // lengths of subarray
int size_ind = ind.size();
for (int k = 1; k < size_ind; ++k) blens.push_back(ind[k] - ind[k - 1]);
sort(blens.begin(), blens.end());
int m = blens.size();
// O(n*sqrt(n))
for (int k = 0; k < m; ) {
int r = k;
while (r < m && blens[r] == blens[k]) ++r;
memset(dp, 0, sizeof(dp));
for (int i = blens[k]; i <= n; ++i) {
if (!vis[i] && vis[i - blens[k]] && dp[i - blens[k]] < r - k) {
dp[i] = dp[i - blens[k]] + 1;
vis[i] = true;
}
}
k = r;
}
/*
O(n*n)
for (int i = 0; i < m; ++i)
for (int j = n; j >= blens[i]; --j)
vis[j] |= vis[j - blens[i]];
*/
cout << (vis[n] ? "YES" : "NO") << endl;
}
return 0;
}
感觉C1能AC但是没想到思路,而且感觉信心也不足,嗨,菜是原罪/(ㄒoㄒ)/~~。
打算长期更新codeforce、leetcode、牛客竞赛,自己还只是个练习时长一年不到的练习生,欢迎同学们交流讨论~