因为版权原因,不放题面。
分组
Solution
dp。
根据数据范围,可以推出复杂度是在 \(O(nk) \sim O(nk^2)\) 之间的。
尽量让组员在组长的前面,所以先根据经验排序,如果经验相同,让志愿为组员的在前面,都可以在中间,志愿为组长的在后面。
设 \(dp_{i,a,b}\) 为排序后前 \(i\) 个人,有 \(a\) 个组长和 \(b\) 个组员的最小代价。对于每个人,根据他的志愿,如果是组长就分进一个只有组员的组,如果是组员就新开一个组,两个都可以就都试一试。
要注意一个边界问题,在分好组及之前,因为排序了,所以不可能有组长的人数比组员多。
发现三维 dp 会 MLE,用滚动数组。还要开 long long。
这样写复杂度为 \(O(nk^2)\),会 TLE。可以特判 \(2 \times k > n\) 的无解情况。复杂度 \(O(nk)\)。
get 了一个 dp 的状态。
Code
#include <bits/stdc++.h>
using namespace std;
#define re register
#define F first
#define S second
typedef long long ll;
typedef pair<int, int> P;
const int N = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int read() {
int x = 0, f = 0; char ch = 0;
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
int n, k;
struct person{
int xp, mon, want;
}a[N];
bool cmp(person a, person b){
if (a.xp != b.xp) return a.xp < b.xp;
return a.want < b.want;
}
ll dp[2][5005][5005];
int main(){
n = read(), k = read();
if (2 * k > n){
puts("-1"); return 0;
}
for (int i = 1; i <= n; i++){
a[i].xp = read(), a[i].mon = read(), a[i].want = read();
if (a[i].want == 1) a[i].want = 3;
else if (a[i].want == 2) a[i].want = 1;
else a[i].want = 2;
}
sort(a + 1, a + n + 1, cmp);
memset(dp, 0x7f, sizeof(dp));
dp[0][0][0] = 0;
int now = 0;
for (int i = 1; i <= n; i++){
now ^= 1;
for (int j = 0; j <= k; j++)
for (int x = 0; x <= j; x++){
dp[now][j][x] = dp[now ^ 1][j][x];
if (j && a[i].want != 3) dp[now][j][x] = min(dp[now][j][x], dp[now ^ 1][j - 1][x] + a[i].mon);
if (x && a[i].want != 1) dp[now][j][x] = min(dp[now][j][x], dp[now ^ 1][j][x - 1] + a[i].mon);
}
}
if (dp[n & 1][k][k] == 9187201950435737471) puts("-1");
else printf("%lld\n", dp[n & 1][k][k]);
return 0;
}
折纸
Solution
横着折纸不会影响和竖着折纸的方案数,竖着折纸也不会影响横着折纸的方案数,所以答案是横着折纸的方案数 \(\times\) 竖着折纸的方案数。
可以在横着折纸时 \(n\) 行看作一行,竖着折纸是 \(m\) 列看作一列,哈希一下就好了。那么问题转换成了只有一行或一列时的方案数,下面讨论一行的,一列同理。
先将折纸转换为,在一行字符串上切一下,令切的位置为 \(x\),然后将短的折向长的,令短的长度为 \(k\),有以 \(x\) 为回文中心的最长回文半径 \(\ge k\)。对于以每个点为回文中心的最长回文半径可以用 manacher 预处理。
折纸可能是从左往右,也可能是从右往左,当找到了从左往右折纸的一个方案时,那么这个方案是一定依赖于前面的方案,所以方案数加一。
但是从右往左的方案不依赖于在它之前从左往右的方案。一种从右往左的方案可以与在它之前能够折出来的所有从左往右的方案匹配。
可以预处理出在每个位置从左往右和从右往左,能不能在这个位置切一下对折,然后对于字符串的所有位置从左往右依次统计方案数即可。时间复杂度为 \(O(nm)\),瓶颈在于读入。
get 了 manacher 和 hash 的神奇用法。
Code
#include <bits/stdc++.h>
using namespace std;
#define re register
#define F first
#define S second
typedef long long ll;
typedef pair<int, int> P;
const int N = 1e6 + 5, Base = 233;
const int INF = 0x3f3f3f3f;
int read() {
int x = 0, f = 0; char ch = 0;
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
int n, m, hsa[N], hsb[N], a[N], p[N];
bool l[N], r[N];
char s[N];
void manacher(int n, int b[]) {
for (int i = 1; i <= n; i++) {
a[i * 2] = b[i], a[i * 2 + 1] = -1;
}
a[0] = -2, a[1] = -1, a[2 * n + 3] = -2;
int id = 0, mx = 0;
for (int i = 1; i <= n * 2 + 1; i++) {
p[i] = i < mx ? min(p[2 * id - i], mx - i) : 1;
while (a[i - p[i]] == a[i + p[i]]) p[i]++;
if (mx < i + p[i]) id = i, mx = i + p[i];
}
for (int i = 0; i <= n; i++) p[i] = p[i * 2 + 1] >> 1;
}
ll solve(int n, int b[]) {
memset(p, 0, sizeof(p)); memset(l, 0, sizeof(l)); memset(r, 0, sizeof(r));
manacher(n, b);
int k = 0;
for (int i = 1; i < n; i++) if (i - p[i] <= k) l[i] = 1, k = i;
k = n;
for (int i = n - 1; i; i--) if (i + p[i] >= k) r[i] = 1, k = i;
k = 1;
ll ans = 0;
for (int i = 1; i <= n; i++){
if (r[i]) ans += k;
if (l[i]) k++;
}
return ans + k;
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) {
scanf("%s", s + 1);
for (int j = 1; j <= m; j++) {
hsa[i] = hsa[i] * Base + s[j] - 48;
hsb[j] = hsb[j] * Base + s[j] - 48;
}
}
printf("%lld\n", solve(n, hsa) * solve(m, hsb));
return 0;
}
集合
Solution
review 一下异或的 trick
\[ a \oplus b \oplus c =a \oplus(b \oplus c) \]
\[ a \oplus b \oplus b = a \]
对于没有加操作的时候,可以维护一个异或的懒标记,当有异或操作的时候懒标记 \(tag \oplus x\),插入删除时,对应将 \(x \oplus tag\) 插入或删除,再最后输出的时候将每一项 \(\oplus tag\)。
考虑加法操作。由于只加一,可以从二进制入手。找规律发现一个数 \(k\) 的二进制位数为 \(\lfloor log_2(k) \rfloor + 1\),我们令一个数的最低位到最高位依次为 \(a_1 \sim a_{30}\),那么 \(+1 \bmod 2^{30}\) 为找到一个满足 \(a_i = 0\) 且 \(a _ 1 \sim a_ {i - 1} = 1\),然后把 \(1\) 变成 \(0\),\(0\) 变成 \(1\),如果不存在满足条件的 \(i\),那么认为 \(i = 31\),那么需要把所有的 \(a_i\) 都反转成 \(0\)。
按照插入数字从低到高维护一棵字典树,用 \(val_x\) 表示字典树节点 \(x\) 对应值出现的次数为 \(val_x\),那么插入和删除操作就是对应的 \(+1\) 或 \(-1\),\(+1\) 就是 \(+tag\), 输入时就直接插入。当找到了满足条件的 \(i\) 时,反转相当于交换 \(i\) 的左右儿子。
get 了 trie 的神奇用法。
Code
#include <bits/stdc++.h>
using namespace std;
#define re register
#define F first
#define S second
typedef long long ll;
typedef pair<int, int> P;
const int N = 6e6 + 5;
const int INF = 0x3f3f3f3f;
int read() {
int x = 0, f = 0; char ch = 0;
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
int n, q, tag, ch[N][2], val[N], ans[N], cnt, tot = 1;
void insert(int k){
int p = 1;
for (int i = 0; i < 30; i++, k >>= 1){
int c = k & 1;
if(!ch[p][c]) ch[p][c] = ++tot;
p = ch[p][c];
}
val[p]++;
}
void erase(int k){
int p = 1;
for (int i = 0; i < 30; i++, k >>= 1) p = ch[p][k & 1];
val[p]--;
}
void add(int k){
int p = 1;
for (int i = 0; i < 30; i++, k >>= 1){
swap(ch[p][0], ch[p][1]);
p = ch[p][k & 1];
}
}
void dfs(int x, int dep, int p){
if(dep == 30){
for (int i = 1; i <= val[x]; i++) ans[++cnt] = p ^ tag;
return ;
}
if (ch[x][0]) dfs(ch[x][0], dep + 1, p);
if (ch[x][1]) dfs(ch[x][1], dep + 1, p | (1 << dep));
}
int main(){
n = read(), q = read();
for (int i = 1; i <= n; i++){
int x = read(); insert(x);
}
while (q--){
int opt = read(), x;
if(opt != 3) x = read();
if(opt == 1) insert(x ^ tag);
if(opt == 2) erase(x ^ tag);
if(opt == 3) add(tag);
if(opt == 4) tag ^= x;
}
dfs(1, 0, 0);
sort(ans + 1, ans + cnt + 1);
for (int i = 1; i <= cnt; i++) printf("%d ", ans[i]);
return 0;
}