前言
这次没复习直接上, 无敌了
还是策略 + 时间分配, 好好考
看题
大样例发的有点神经的
\(\rm{T1}\)
什么数数题, 红温了, 不太看得出来的样子
\(\rm{T2}\)
有点神秘
\(\rm{T3}\)
又是数数, 无语了
\(\rm{T4}\)
神秘
首先应该是普通难度, 打不了的题先跳过, 先拿暴力分, 不要死磕
\(\rm{T1}\)
思路
作为一个刚学了一堆数数 \(\rm{trick}\) 的人, 看看能不能冲一下正解
转化题意
对于一个 \(n\) 行 \(m\) 列的 \(01\) 矩阵, 求有多少种行数的集合, 使得翻转这些行以后每一列至多有一个 \(1\)
\(60 \%\) 给状压, 好像还不是很稳
通过简单转化我们可以知道, 对于 \(m\) 列, 有哪些集合及集合的反转会使这一位有 \(1\), 记为 \(\mathbb{U}\)
考虑正难则反算至少有一列至少有两个 \(1\) 的情况, 也就是说, 我们随意挑选至少一列的 \(\mathbb{U}\) 中的两个集合 (不能是一个集合和其反转不能同时选择) , 就可以产生一种非法情况
考虑非法情况的计数
好吧时间不够了, 先考虑暴力, 最后有时间再想正解
对于 \(20 \%\) , 先处理选择的行的集合, 然后 \(\mathcal{O} (nm)\) \(\rm{check}\) 即可, 总时间复杂度 \(\mathcal{O} (2^n nm)\)
对于 \(40 \%\) , 我们考虑状压 \(\rm{dp}\)
令 \(f_{i, \mathbb{S}}\) 表示考虑了前 \(i\) 行, 当前的放 \(1\) 情况为 \(\mathbb{S}\) 时的方案数
考虑转移, 记 \(s_i\) 表示第 \(i\) 行不反转时 \(1\) 的位置集合, \(s^{\prime}_i\) 表示第 \(i\) 行反转时 \(1\) 的位置集合
总时间复杂度 \(\mathcal{O} (n 2^m)\)
实现
框架
实现两个部分
- \(n\) 较小时
暴力模拟即可 - \(m\) 较小时
如上状压 \(\rm{dp}\) 即可
代码
/*对于 n 较小时的做法*/
class subtask1
{
private:bool chose[MAXN];bool check(int S) {int cnt = 0;for (int i = 1; i <= n; i++) chose[i] = (S >> (i - 1)) & 1;for (int i = 1; i <= m; i++) {int res = 0;for (int j = 1; j <= n; j++) {if (chose[j]) res += (mat[j][m - i + 1] == '1');else res += (mat[j][i] == '1');}if (res > 1) return false;}return true;}public:void solve() {int ans = 0;for (int S = 0; S < (1 << n); S++) // 外层枚举选行的状态if (check(S)) ans++;printf("%lld\n", ans);}
} sub1;/*对于 m 较小时的做法*/
class subtask2
{
private:int f[2][MAXVAL];int s1[MAXN], s2[MAXN];/*预处理 s_i, s'_i*/void init() {for (int i = 1; i <= n; i++) {s1[i] = s2[i] = 0;for (int j = 1; j <= m; j++) {if (mat[i][j] == '1') s1[i] += (1 << (j - 1));if (mat[i][m - j + 1] == '1') s2[i] += (1 << (j - 1));}}}public:void solve() {init();int now = 0, nxt = 1;memset(f, 0, sizeof(f)); f[now][0] = 1;for (int i = 0; i < n; i++) {memset(f[nxt], 0, sizeof(f[nxt]));for (int S = 0; S < (1 << m); S++) {if (!(S & s1[i + 1])) f[nxt][S | s1[i + 1]] += f[now][S];if (!(S & s2[i + 1])) f[nxt][S | s2[i + 1]] += f[now][S];}std::swap(now, nxt);}int ans = 0;for (int S = 0; S < (1 << m); S++) ans += f[now][S];printf("%lld\n", ans);}
} sub2;
\(\rm{T2}\)
目前得分: \(60 \ \rm{pts}\)
思路
不是数数, 加油冲冲冲
看到这个形式就想转差分, 不管怎么样先用这个思路转化题意
给定一个序列 \(a\) , 每次操作你可以任取 \(i \in [1, n]\) , 将 \(a_i + 1, a_{i + 1} - 1\) 或者将 \(a_i - 1, a_{i + 1} + 1\) , 求至多 \(k\) 此操作后, 最长的连续 \(1\) 有多长 ( \(a_1, a_{n + 1}\) 不计入统计)
你发现 \(k\) 很大, 也属于一种提醒, 不能一次一次模拟
\(666\) , 根本不会做
实现
考虑 \(10 \%\)
#include <bits/stdc++.h>
const int MAXN = 1e5 + 20;
int T;
int n, k, a[MAXN], dif[MAXN];int main()
{scanf("%d", &T);while (T--) {scanf("%d %d", &n, &k);for (int i = 1; i <= n; i++) scanf("%d", &a[i]), dif[i] = a[i] - a[i - 1];int ans = 0;int res = 0;for (int i = 2; i <= n; i++) {if (dif[i] == 1) { res++; ans = std::max(res, ans); }else res = 0;}printf("%d\n", ans + 1);}return 0;
}
\(\rm{T4}\)
目前得分: \(70 \ \rm{pts}\)
思路
这题能骗多少骗多少
\(30 \%\) 给了暴力模拟, 维护每个串串当前的首位, 然后暴力计算即可
考虑 \(n = 2\) , 暴力枚举会 \(\rm{TLE}\) , 怎么做
考虑 \(l_i \leq 2\) , 暴力枚举会 \(\rm{TLE}\) , 怎么做
急急急, 把 \(30 \%\) 打完去冲 \(\rm{T3}\) 的 \(\mathcal{O} (nk)\)
实现
框架
模拟模拟
代码
#include <bits/stdc++.h>
const int MAXN = 6;
const int MAXL = 6;
#define lcm(a, b) a / std::__gcd(a, b) * bint T;
int n, m;
int a[MAXN][MAXL];
int l[MAXN];int main()
{scanf("%d", &T);while (T--) {scanf("%d %d", &n, &m);int times = 1;for (int i = 1; i <= n; i++) {scanf("%d", &l[i]); times = lcm(times, l[i]);for (int j = 1; j <= l[i]; j++) scanf("%d", &a[i][j]);}for (int i = 1; i <= m; i++) {int ans = 0;int head[MAXN];memset(head, 0, sizeof(head));for (int j = 1; j <= times; j++) {for (int k = 1; k <= n; k++) head[k] = ((head[k] + 1) > l[k] ? 1 : head[k] + 1);int res = 0;for (int k = 1; k <= n; k++) {if (a[k][head[k]] == i) { res++; ans = std::max(ans, res); }else res = 0;}}printf("%d ", ans);}printf("\n");}return 0;
}
\(\rm{T3}\)
目前得分: \(100 \ \rm{pts}\)
思路
现在优势一点都不在我, 主要是 \(\rm{T2}\) 基本上啥都不会, 寄!
又是数数, 看看这题够不够典
转化题意
给定一棵 \(n\) 节点的树, 求删边边集的数量, 使得剩余部分的连通块大小均为 \(k\) 或者 \(k + 1\)
怎么又是和 \(\rm{T1}\) 很像的形式, 不是哥们
不过这个题像树形 \(\rm{dp}\)
好吧也是再次没想出来
考虑 \(10 \%\) 直接状态压缩之后 \(\mathcal{O} (n)\) \(\rm{check}\)
还有个链的 \(10 \%\) 考虑动态规划
具体的, 令 \(f_{u, s}\) 表示对于 \(u\) 这个点, 其带了一个大小为 \(s\) 的连续块过来的方案数
初始化 \(f_{leave, 1} = 1\)
考虑转移 (假设 \(u\) 的下一个点为 \(v\) )
然后发现数组开不下? 直接打完 \(10 \%\) 跑路, 不过这个好像可以拓展成 \(60 \%\)
不过问题是 \(\rm{T4}\) 部分分很足, 先开 \(\rm{T4}\) , 这 \(10 \%\) 留到后面, 还有时间就把 \(60 \%\) 打了
好好好, 剩余的时间全部冲这题的 \(\mathcal{O} (nk)\) , 先手模一遍确定正确性
我样例呢?!
好好好, 正确性没问题, 但转移方式还要细想
考虑对于 \(u\) 子树及其儿子 \(son(u)\) , 怎么转移
容易发现的是我们可以一颗子树一颗子树的加入, 不影响答案
考虑对于 \(f_{u, i}\) , 会从哪些 \(v\) 的地方产生贡献
怎么怎么搞都是 \(\mathcal{O} (n k^2)\) 的, 红温了
考虑类似树上背包的形式, 还是只能到 \(\mathcal{O} (n k^2)\)
实现
只能打个暴力了
#include <bits/stdc++.h>
const int MAXN = 21;
const int MOD = 998244353;int n, k;
struct node {int to, next;
} edge[MAXN << 1];
int cnt = 0, head[MAXN];
void init() {cnt = 0;for (int i = 1; i <= n; i++) head[i] = -1;
}void addedge(int u, int v) {edge[++cnt].to = v, edge[cnt].next = head[u];head[u] = cnt;
}int sz;
bool canuse[MAXN << 1];
bool vis[MAXN];
void dfs(int now) {sz++;vis[now] = true;for (int i = head[now]; ~i; i = edge[i].next) {int nowto = edge[i].to;if (vis[nowto] || !canuse[i]) continue;dfs(nowto);}
}bool check(int s) {memset(canuse, false, sizeof(canuse)); memset(vis, false, sizeof(vis));int ret = 0;while (s) {ret++;if (s & 1) {canuse[ret] = canuse[ret + 1] = true;} else {canuse[ret] = canuse[ret + 1] = false;}ret++;s >>= 1;}for (int i = 1; i <= n; i++) {sz = 0;if (!vis[i]) dfs(i);if (sz != k + 1 && sz != k && sz) return false;}return true;
}int main()
{int T; scanf("%d", &T);while (T--) {scanf("%d %d", &n, &k);init();for (int i = 1; i < n; i++) {int u, v;scanf("%d %d", &u, &v);addedge(u, v), addedge(v, u);}int ans = 0;for (int i = 0; i < (1 << (n - 1)); i++) {if (check(i)) ans = (ans + 1) % MOD;}printf("%d\n", ans);}return 0;
}
后记
\(\rm{qtmd}\) , 破大防