题意:给出一个长度为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
我们可以注意到,此类转移对于 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
题解(翻译+补充):
直接贪心会出问题。如果尽量贪这个数是最大的,然后其他次大的如何分配最小就很难保证。
首先,我们计算每个牌手还需要拿多少张牌,对第 i i i个牌手,记作 r e m a i n i remain_i remaini。我们想要让第 i i i个选手获得的分数最大。我们可以遍历四种花色,让这个选手获得尽可能多的牌。【如果他最后的牌数没拿满到最大的,当我们从其他牌手拿到一张这个花色的牌,并且用另一种花色交换,如果拿到一张牌还是次大,0分不变,如果变成最大了,至少非负,先前是0分,如果最大更大了,+1分,但是如果给的花色刚好是次大的花色牌子,那么又-1分,总之不会变坏。】【就是我们遍历第一个选手所有可能选择的花色】
此时,我们想要找到一种存在的分配方法,使得其他牌手手里剩余的牌的最大值最小。假定是 x x x。需要合理地分配剩余的牌,使得选手 k k k,获得 r e m a i n k remain_k remaink张牌,并且没有任何人拥有超过 x x x张相同花色的牌(除了第 i i i个选手)。
对于
x
x
x,它具有单调性【如果某种花色的最大可能分配到某人手上的牌数有
n
n
n,如果我们可以通过分配使得其他选手的某种花色的最大值为
x
(
其中
x
<
n
)
x(其中x
此时,我们可以解决下面这个问题:检查是否存在一种卡片的分配,构造出上述的 x x x的情况,我们可以使用最大流来解决。
但是,简单的建立网络的方法太慢了,更不要说找到其中的最大流。我们需要某种更快的方法。
如果你想用Hall定理,但是不幸的是,Hall定理无法解决 x x x的约束,但是你思考的方向是正确的。
让我们尝试去找到这个网络的最小割。为了做到我们遍历状态码 m a s k mask mask关于四种花色在最小割的集合 S S S里面,然后将剩余的花色的容量加入到这个最小割上,对每个牌手,判断是否有有选定集合里的更好还是最小割其他的集合 T T T更好。关键的事情是,如果我们固定了 m a s k mask mask和 x x x,这些答案是固定的。把选中的花色的数目记作 s u i t s suits suits,另外第 k k k个牌手已经拥有的牌的花色和数目记作 a l r e a d y H a s m a s k , k alreadyHas_{mask,k} alreadyHasmask,k。全部指向运动员边的第 k k k个牌手的变得容量和,我们需要去割的部分是 s u i t s ⋅ x − a l r e a d y H a s m a s k , k suits ·x-alreadyHas_{mask,k} suits⋅x−alreadyHasmask,k 然后出边是 r e m a i n k remain_k remaink。我们需要添加的割就是两者取小。我们需要高效地计算这些值的和。
这是解法的基本大纲,现在需要实现。
对于每个 m a s k mask mask的每个值 x x x,我们找到 s u i t s ⋅ x − a l r e a d y H a s m a s k , k suits ·x-alreadyHas_{mask,k} suits⋅x−alreadyHasmask,k 的和。为了做到这个,对每个运动员,找到最大的 x x x,我们可以计算那些线性函数的和。和不超过 2 e 6 2e6 2e6。
当我们计算对第 i i i个牌手的最小割时(确定 m a s k mask mask和 x x x),我们可以用已经计算号的函数来找到所有牌手的和,加入到最小割上,之后减去第 i i i个牌手添加的。
注意,当我们通过二分来找到 x x x的左边界的时候,要计算其他其他牌手最大的能拥有的花色的牌数,保证 x x x不会让容量变成负数。
这个模型的预处理的时间复杂度是 O ( 2 K ⋅ ( n ⋅ K + A ) ) O(2^K·(n·K+A)) O(2K⋅(n⋅K+A)),实际的复杂度是 O ( n ⋅ 2 K ⋅ K 2 ⋅ log A ) O(n·2^K·K^2·\log A) O(n⋅2K⋅K2⋅logA)其中 A < 2 e 6 A<2e6 A<2e6