题意:给出一个长度为9,含有1-9字符的串,求一个长度大于等于2的子序列,这个子序列是素数。
思路:取13或者取31
代码:
void solve() {
string s;
cin >> s;
for (auto &x : s) {
if (x == '1' || x == '3') cout << x;
}
cout << '\n';
}
题意:给定两个01串,现有操作,每次可以选择两个相同的字符(同一字符串),并把这两个字符之间的位置都填上这个字符。让你判断这两个字符串通过若干这样的操作能否变成相同的字符串。注意,最左端是0,最右端是1。
思路:实际上,经过操作后我们可以把这些字符串分成若干0块和1块(连续的含有0或1的子串),实际上这种情况再操作一次,取最左侧的1块的第一个1和最右端的1,就能转化成00000111111111的串,任何相同的串都可以转化成这种串,我们判断能否转化成这样的01串就行。枚举所有1的位置,判断能否转化,如果一个是1,一个不是1,那么必定不行,这样没有1的那个串就无法把1串延展的恰好这个位置,要么左侧1,右侧1,必须都是1,同样的,保证0等延伸到这也需要保持相同。
代码:
void solve() {
string a, b;
cin >> a >> b;
int n = a.size();
for (int i = 1; i < n; i++) {
if (a[i] == b[i] && a[i] == '1' && a[i - 1] == b[i - 1] && a[i - 1] == '0') {
cout << "YES\n";
return;
}
}
cout << "NO\n";
}
题意:有三种操作,+操作,在数组末尾添加一个数,-操作,移除末尾一个数,0/1操作,检查数组是否是单调不减的,是写1,不是写0。问是否可能出现。
补充:逆序是指较小的数在较大的数后面。
思路:关键是01的位置。以下提供我的思路,首先将字符串划分成一个个含有+±-0/1的子串,因为每个0,1的判断相关联的是部分就是前面的+和-的部分。我维护两个栈,一个栈bad是产生逆序的位置,非空,就会造成这个当前序列不是排序的;一个栈may是记录可能产生逆序的位置。
分两种情况,如果当前子串末尾是1,这种情况比较简单,此时,may栈要清空,因为已经认定当前位置所有数有序了,接下来模拟±操作,用cnt记录当前末端的数是第几个数,当-的时候,检查bad栈顶是否有相同元素,有则弹出,删除了一个已经存在的逆序数。
如果当前子串的末尾是0,这种时候要仔细一些。分两种小清空,首先,如果bad栈是空的,为了让0合法,必须找个位置设置逆序,对于0我们有多个位置选择逆序,这些位置对于0而言都是等价的,但是必须取,但是对于后续的1的影响是不同的,为了后面的1更好的合法,后面的1所更改的范围是有限的,为了更好地覆盖前面逆序的位置,我们选取逆序的位置必须尽量靠后,所以我们取may栈顶的数作为逆序位置。如果may栈是空的,也就是前面有1,或者本次操作内没有可以放置逆序的地方,就是非法的。在本次循环中,如果+的话我们往may栈内增加,-的话要相应减去,确保本次操作可能的逆序的位置入栈。
代码:
void solve() {
string s;
cin >> s;
int n = s.size();
vector<string> op;
string tmp;
for (int i = 0; i < n; i++) {
tmp += s[i];
if (s[i] == '0' || s[i] == '1') {
op.push_back(tmp);
tmp = "";
}
}
stack<int> bad, may;
int cnt = 0;
for (auto &x : op) {
if (x.back() == '1') {
int n = x.size();
for (int i = 0; i + 1 < n; i++) {
if (x[i] == '+') cnt++;
else {
if (!bad.empty() && bad.top() == cnt) {
bad.pop();
}
cnt--;
}
}
while (!may.empty()) may.pop();
if (!bad.empty()) {
cout << "NO\n";
return;
}
} else {
int n = x.size();
for (int i = 0; i + 1 < n; i++) {
if (x[i] == '+') {
cnt++;
if (cnt > 1)
may.push(cnt);
} else {
if (!bad.empty() && bad.top() == cnt) {
bad.pop();
}
if (!may.empty() && may.top() == cnt) {
may.pop();
}
cnt--;
}
}
if (bad.empty()) {
if (may.empty()) {
cout << "NO\n";
return;
} else {
bad.push(may.top());
}
}
}
}
cout << "YES\n";
}
题意:给出一个数组,每次操作可以选择一个区间乘上一个数(可正可负),最后使得这个数组有序。问最少要操作几次。
思路:首先,我们考虑全都是正数的情况,如果有一个 a i ⩾ a i + 1 a_i \geqslant a_{i + 1} ai⩾ai+1我们至少要操作一次,如果有两个 a i ⩾ a i + 1 a_i\geqslant a_{i+1} ai⩾ai+1我们至少要操作两次,同样地,有 k k k个就需要操作 k k k次。可以用数学归纳法证明。此外,关于负数的情况,首先操作后必然是一段连续的负数(或者是空),如果是多段负数,中间的正数必然是非法的,不符合最优的原则。如果有负数的话,我们首先给一个前缀乘上一个负数,这样操作之后,负数前缀的数必定小于正数后缀的数,接下来保证两者内部都是有序的,负数和正数的情况恰好相反,枚举所有的负数前缀,统计使得前缀和后缀有序的操作数,取最小。
另外:为什么可以先乘一个负数,对于选取的前缀,需要让前缀全负并且升序,我们可以按照 a i ⩽ a i + 1 a_i\leqslant a_{i + 1} ai⩽ai+1划分乘一段一段,分别乘上合适的负数,最后多出段前缀的非负数,其实等效于我事先乘上一个负数,取全部的选择的负数前缀。
代码:
void solve() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
vector<int> l(n), r(n + 1);
l[0] = 0;
for (int i = 1; i < n; i++) {
l[i] = l[i - 1] + (a[i] >= a[i - 1]);
}
r[n - 1] = r[n] = 0;
for (int i = n - 2; i >= 0; i--) {
r[i] = r[i + 1] + (a[i] >= a[i + 1]);
}
int ans = r[0];
for (int i = 0; i < n; i++) {
ans = min(ans, l[i] + r[i + 1] + 1);
}
cout << ans << '\n';
}
题意:给你两个数n,k,问长度为n数组,里面是任意的1到k的数,所有出现长度为k连续的两两相同的子数组的数目。
思路:意译了一下官方题解,并且补充了一些说明。
题解:
首先考虑给出一个确定的数组和k,如何计算这个数目呢?可以考虑贪心,尽可能选择靠左的子数组,这样剩余的数越多,更可能选择出更多的子数组。利用哈希表,判断是否出现过当前的数,可以在 O ( n ) O(n) O(n)的时间复杂内实现。我们的目标是选择出后缀k个数不同,我们需要判断当前的数是否出过,出现过需要调整左侧指针,指向上一个相同的数出现的位置后一个数,没有出现过后缀长度会增长1,如果抵达k后缀长度要减到0,答案增加1,因为我们不能重复使用这些数。
设计 d p i , x , c dp_{i,x,c} dpi,x,c是当前长度为 i i i,后缀两两不同的数长度为 x x x,当前有效子数组的数目是 c c c的状态的数组数目。那么此时最后的答案便是 ∑ x = 0 k − 1 ∑ c = 1 n k d p n , x , c ⋅ c \sum^{k-1}_{x=0}\sum^{\frac{n}{k}}_{c=1}{dp_{n,x,c}\cdot c} ∑x=0k−1∑c=1kndpn,x,c⋅c,答案的含义是,对长度为 n n n,后缀有效数字 0 ∼ k − 1 0\sim k-1 0∼k−1的情况,对所有不同价值的数组数对应求和。
d p i , x , c dp_{i,x,c} dpi,x,c转移有两种。
新的数不出现在长为 x x x的后缀当中,这种情况比较简单:
新的数出现在长为 x x x的后缀当中,这样并不会增加 x x x或者价值数,不会增加有效后缀长度。对于一个长度为 x x x的后缀,这个新出现的数有 1 ∼ x 1\sim x 1∼x种可能,对于所有出现的数,这个数再原先后缀的位置可能是倒数第 1 ∼ x 1\sim x 1∼x位置。因此可能转移到 d p i + 1 , 1 ∼ x , c dp_{i+1,1\sim x,c} dpi+1,1∼x,c,但是直接转移的时间复杂度就是 O ( n k 2 ) O(nk^2) O(nk2),为了减小复杂度,考虑前缀和。注意看。
对于 k − 1 ,转移的范围是 1 2 3 … k − 3 k − 2 k − 1 对于 k − 2 ,转移的范围是 1 2 3 … k − 3 k − 2 对于 k − 3 ,转移的范围是 1 2 3 … k − 3 … 对于 3 ,转移的范围是 1 2 3 对于 2 ,转移的范围是 1 2 对于 1 ,转移的范围是 1 \begin{align*} 对于k-1,转移的范围是 &1\quad 2\quad 3 \dots k-3 \quad k-2 \quad k-1\\ 对于k-2,转移的范围是 &1\quad 2\quad 3 \dots k-3 \quad k-2\\ 对于k-3,转移的范围是 &1\quad 2\quad 3 \dots k-3\\ \dots\\ 对于3,转移的范围是 &1\quad 2\quad 3\\ 对于2,转移的范围是 &1\quad 2\\ 对于1,转移的范围是 &1\\ \end{align*} 对于k−1,转移的范围是对于k−2,转移的范围是对于k−3,转移的范围是…对于3,转移的范围是对于2,转移的范围是对于1,转移的范围是123…k−3k−2k−1123…k−3k−2123…k−3123121
我们可以注意到,此类转移对于 d p i + 1 , j , c dp_{i+1,j,c} dpi+1,j,c的贡献。就是对应某列,我们可以用前缀的思想,首先取转移范围最大的,依次累加,可以在 O ( k ) O(k) O(k)的时间复杂度内,得到各列所得到的来自 d p i , x , c dp_{i,x,c} dpi,x,c贡献。
上面的方法已经可以通过了,注意此处的 c c c的上限是 n k \frac{n}{k} kn,所以三维的总空间复杂度是 O ( n 2 ) O(n^2) O(n2)
实际上,第三维度可以省略,每次我们凑出一个完整的后缀的时候,直接计算当前这个后缀对最后的答案的贡献,相当于把原来最后求答案的过程,分散到循环当中,边递推边求答案。 d p i , j dp_{i,j} dpi,j的含义是前 i i i个数,有长度为 j j j的有效后缀,的前缀数组数目。 j = 0 j=0 j=0的时候,就是在当前节点为结尾的长度为 k k k的后缀,的前 i i i个数的排列组合数,乘以后面的其他任意组合的数,就是此处结尾 k k k个数的贡献。注意,后面的数可以随意取,因为即使里面出现了另外的 k k k个的数,并不会重复计算。因为后者一种情况和此处的后缀互斥,不冲突。即使可以同时存在,这两者的贡献都是独立的,并不矛盾。都是合法的答案。
代码:
ll po(ll rad, ll idx) {
ll res = 1;
while (idx) {
if (idx & 1) res = res * rad % mod;
rad = rad * rad % mod;
idx >>= 1;
}
return res;
}
int add(int x, int y) {
return (x + y) % mod;
}
void solve() {
int n, k;
cin >> n >> k;
vector<vector<int>> f(n + 1, vector<int> (k + 1, 0));
f[0][0] = 1;
int ans = 0;
for (int i = 0; i < n; i++) {
int cur = 0;
for (int j = k - 1; j >= 1; j--) {
cur = add(cur, f[i][j]);
f[i + 1][j] = cur;
}
for (int j = k - 1; j >= 0; j--) {
int nxt = (j + 1) % k;
f[i + 1][nxt] = add(f[i + 1][nxt], 1ll * f[i][j] * (k - j) % mod);
}
ans = add(ans, 1ll * f[i + 1][0] * po(k, n - i - 1) % mod);
}
cout << ans << '\n';
}
题意:有 n + 1 n+1 n+1个人, n n n个牌手, 1 1 1个发牌人。发牌人有一副牌,包含四种花色(不同花色的牌数目不一定相同)。这副牌的总数可以被 n n n整除。发牌人把全部的牌等分给每个牌手。然后,每个牌手,选择保留手上牌的一种花色,其余牌都弃置。此时比较每个人牌数,拥有最大牌数的人获得最大牌数减去次大牌数的积分,其余人获得零积分。
每个人都想赢,必定会选择手上牌数最多的花色保留。
现在,发牌人已经给了一些牌给选手。第 i i i个选手获得 a i , j a_{i,j} ai,j张 j j j花色的牌。此时每个牌手手里的牌数不必相等,因为还没有发完牌。此时告诉你发牌人手上对应花色 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4剩余的牌 b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4。现在请你计算针对每一个牌手的最佳发牌方式,使得其获得最大的分数。
输入:一个数 n n n,一个 n × 4 n\times4 n×4的矩阵, b 1 , b 2 , b 3 , b 4 b_1,b_2,b_3,b_4 b1,b2,b3,b4四个数。
输出: n n n个数,每个牌手最大积分。
范围: 2 ⩽ n ⩽ 5 e 4 2\leqslant n\leqslant 5e4 2⩽n⩽5e4, 0 ⩽ a i , j , b j ⩽ 1 e 6 0\leqslant a_{i,j},b_j\leqslant 1e6 0⩽ai,j,bj⩽1e6