T1 洛谷 U490727 返乡
思路
首先要意识到一个问题,就是如果所有人总分一定,那么是不会出现偏序的。
可以感性理解一下,就是对于 i,ji, ji,j, 若 ai≤aj,bi≤bja_i \leq a_j, b_i \leq b_jai≤aj,bi≤bj,那么一定会有 ci≥cjc_i \geq c_jci≥cj。然后枚举方案时,保证不会出现 ai==aj&bi==bja_i == a_j \& b_i == b_jai==aj&bi==bj 这种情况就行。
然后就是看在总分为多少时,方案数最多。
考试的时候可以先写一个暴力程序,先枚举总分 s∈[0,3n]s \in [0, 3n]s∈[0,3n],然后枚举前两科分数 i,j∈[0,n]i, j \in [0, n]i,j∈[0,n],若算下来第三科成绩 s−i−js - i - js−i−j 也小于等于 nnn,那就是一种合法情况,记录下方案数,最后求最大值。
复杂度 O(3n3)O(3n^3)O(3n3),代码如下:
#include <bits/stdc++.h>#define mkpr make_pair
#define fir first
#define sec secondusing namespace std;typedef long long ll;const int maxn = 1e6 + 7;
const int inf = 0x3f3f3f3f;int n, ansc, anss;
int ans[maxn][3];
int main() {scanf("%d", &n);for (int s = 0; s <= 3 * n; ++s) {int cnt = 0;for (int i = 0; i <= n; ++i)for (int j = 0; j <= n; ++j)if (i + j <= s && s - i - j <= n) ++cnt;if (cnt > ansc) {anss = s; // 记录下方案最多时的总分,找找规律ansc = cnt;cnt = 0;for (int i = 0; i <= n; ++i)for (int j = 0; j <= n; ++j)if (i + j <= s && s - i - j <= n) {++cnt,ans[cnt][0] = i;ans[cnt][1] = j;ans[cnt][2] = s - i - j;}}}printf("anss:%d, ansc:%d\n", anss, ansc);for (int i = 1; i <= ansc; ++i)printf("%d %d %d\n", ans[i][0], ans[i][1], ans[i][2]);return 0;
}
n≤600n \leq 600n≤600,由于有常数 333,所以过不去。
然后会有规律:s=3n2s = \frac{3n}{2}s=23n 时,方案数最多。(笔者有点不太会证,先鸽着。。。)
所以直接用如下代码:
#include <bits/stdc++.h>#define mkpr make_pair
#define fir first
#define sec secondusing namespace std;typedef long long ll;const int maxn = 1e6 + 7;
const int inf = 0x3f3f3f3f;int n, ansc;
int ans[maxn][3];
int main() {scanf("%d", &n);for (int i = 0; i <= n; ++i) {for (int j = 0; j <= n; ++j) {int k = n * 3 / 2 - i - j;if (k >= 0 && k <= n) {++ansc;ans[ansc][0] = i;ans[ansc][1] = j;ans[ansc][2] = k;}}} printf("%d\n", ansc);for (int i = 1; i <= ansc; ++i)printf("%d %d %d\n", ans[i][0], ans[i][1], ans[i][2]);return 0;
}
O(n2)O(n ^ 2)O(n2) 水过。
T2 洛谷 U490729 连接
相当沟槽好的一道题,让我花了一天改。。。
还是要喷一下题解,高中肄业都写不出那样生理紊乱的表达。。。
还好有同机房的大佬给出的解法。
一、15pts15pts15pts 思路(n,li,pi≤10,O((∑i=1nli)2)n, l_i, p_i \leq 10,O((\sum_{i = 1}^{n}l_i)^2)n,li,pi≤10,O((∑i=1nli)2))
可以直接用数组把钢管模拟出来,处理出质量与长度的前缀和数组后,直接 (∑i=1nli)2(\sum_{i = 1}^{n}l_i)^2(∑i=1nli)2 暴力枚举求密度。
代码如下:
int sl, sm[107];
double ans;
void Main() {for (int i = 1; i <= n; ++i) {for (int j = sl + 1; j <= sl + len[i]; ++j)sm[j] = sm[j - 1] + p[i];sl += len[i];}for (int i = 1; i <= sl; ++i) {for (int j = i; j <= sl; ++j) {if (sm[j] - sm[i - 1] >= L && sm[j] - sm[i - 1] <= R)ans = max(ans, 1.0 * (sm[j] - sm[i - 1]) / (j - i + 1));}}printf("%.10lf\n", ans);
}
二、50pts50pts50pts 思路(n≤5000,O(n2)n \leq 5000, O(n^2)n≤5000,O(n2))
n2n^2n2 的做法要求我们以完整的块为单位枚举求解。
思考两种截取方法,一种是截整块(即若干个连续的块完整地截下来),另一种是带散块(即两端的钢管不会被截完)。
对于前者,依旧用前缀和直接 O(n2)O(n^2)O(n2) 求解。对于后者,要意识到两件事:
- 最多只会有一端是散块,不会两端都是散块:因为在质量允许的范围内,我们肯定更倾向于去两端中密度更大的那一个,那样可以使整体密度更大(这点根据生活经验或者糖水不等式);
- 散块的质量只会有 LLL 或 RRR:因为如果散块的密度大,我们肯定希望多取,那就直接取到 RRR,可以使整体密度最大化。如果密度小,那我们肯定希望少取,那就直接取到 LLL,防止其继续拉低整体密度。
思路大概如此,实现时由于散块可能是两端中的任意一个,所以要给 li,pil_i, p_ili,pi 数组反转后再求解一次。枚举左端点 iii,然后用双指针或二分查找维护右端点取值范围,再直接计算散块密度。
代码如下:
#include <bits/stdc++.h>using namespace std;typedef long long ll;
typedef double db;const int maxn = 3e5 + 7;int n, len[maxn], p[maxn];
ll L, R, sm[maxn], sl[maxn];
db ans;void solve() {for (int i = 1; i <= n; ++i)sl[i] = sl[i - 1] + len[i],sm[i] = sm[i - 1] + 1ll * len[i] * p[i];// printf("sl: ");
// for (int i = 1; i <= n; ++i) printf("%d ", sl[i]);
// printf("\n");
//
// printf("sm: ");
// for (int i = 1; i <= n; ++i) printf("%d ", sm[i]);
// printf("\n");// 算一端是散块的情况int l = 1, r = 0;for (int i = 1; i <= n; ++i) {// 必须满足质量大于 Lwhile (l <= n && sm[l] - sm[i - 1] < L) ++l;// 这里 r 的判断条件必须是 r <= n 而不是 r + 1 <= n// 因为后面要判断质量能不能取到 Rwhile (r <= n && sm[r + 1] - sm[i - 1] < R) ++r;// 再怎么取都无法达到 Lif (l > n) break;// 分子是质量, 分母是长度db pl = 1.0 * L / (sl[l - 1] - sl[i - 1] + (L - (sm[l - 1] - sm[i - 1])) * 1.0 / p[l]);db pr = 1.0 * R / (sl[r - 1] - sl[i - 1] + (R - (sm[r - 1] - sm[i - 1])) * 1.0 / p[r]);// 如果质量取不到 R, 那就不能用 R 为质量来算密度if (r > n) pr = 0.0;ans = max(ans, max(pl, pr));}// 算选的都是整块的情况for (int i = 1; i <= n; ++i) {for (int j = i; j <= n; ++j) {ll m = sm[j] - sm[i - 1];if (m >= L && m <= R) {ans = max(ans, 1.0 * m / (sl[j] - sl[i - 1]));
// printf("%lf\n", 1.0 * m / (sl[j] - sl[i - 1]));}else if (m >= L && m > R && i == j)ans = max(ans, p[i] * 1.0);
// printf("[%d, %d], ans:%lf\n", i, j, ans);}}
}
int main() {
// freopen("connect2.in", "r", stdin);scanf("%d%lld%lld", &n, &L, &R);for (int i = 1; i <= n; ++i) scanf("%d", len + i);for (int i = 1; i <= n; ++i) scanf("%d", p + i);// puts("|||||||||||||||||||||||||||||||||||||||||||||");solve();reverse(len + 1, len + n + 1);reverse(p + 1, p + n + 1);
// puts("|||||||||||||||||||||||||||||||||||||||||||||");solve();printf("%.10lf\n", ans);return 0;
}
三、正解(O(n×log(max{pi})O(n \times log(max\left \{p_i \right \})O(n×log(max{pi}))
O(n2)O(n^2)O(n2) 的做法在于整块,所以优化这一部分。
机房大佬给出的解法是先二分密度 midmidmid,然后再 chkchkchk。
chkchkchk 函数依旧先枚举左端点,然后二分查找或双指针维护右端点取值范围 [l,r][l,r][l,r],然后再遍历 [l,r][l, r][l,r],看看其中是否存在一个 jjj 满足 smj−smi−1slj−sli−1≥mid\frac{sm_j - sm_{i - 1}}{sl_j - sl_{i - 1}} \geq midslj−sli−1smj−smi−1≥mid。但是这样本质上依旧是 O(n2)O(n^2)O(n2) 暴力。
把判断式子化简一下,成这样:mid×sli−1−smi−1≥mid×slj−smjmid \times sl_{i - 1} - sm_{i - 1} \geq mid \times sl_j - sm_jmid×sli−1−smi−1≥mid×slj−smj。发现我们判断的是区间中的最小值是否小于 mid×sli−1−smi−1mid \times sl_{i - 1} - sm_{i - 1}mid×sli−1−smi−1,那就用单调队列维护最小值就好了。
代码如下:
#include <bits/stdc++.h>using namespace std;typedef long long ll;
typedef long double ld;
typedef double db;const int maxn = 3e5 + 7;int n, len[maxn], p[maxn];
ll L, R, sm[maxn], sl[maxn];
db ans;int q[maxn], h, t;
bool chk(ld x) {memset(q, 0, sizeof(q));h = 1, t = 0;for (int i = 1, r = 0; i <= n; ++i) {// 单调队列扩展右区间while (r + 1 <= n && sm[r + 1] - sm[i - 1] <= R) {++r;while (h <= t && x * sl[q[t]] - sm[q[t]] > x * sl[r] - sm[r]) --t;q[++t] = r;}// 把不合法的左区间删了while (h <= t && sm[q[h]] - sm[i - 1] < L) ++h;if (h > t) continue;// 满足上述式子if (x * (ld)sl[i - 1] - (ld)sm[i - 1] >= x * (ld)sl[q[h]] - (ld)sm[q[h]])return true;}return false;
}
void solve() {for (int i = 1; i <= n; ++i)sl[i] = sl[i - 1] + len[i],sm[i] = sm[i - 1] + 1ll * len[i] * p[i];// 算一端是散块的情况for (int i = 1, l = 1, r = 0; i <= n; ++i) {// 单个块质量就超出 R 的话直接特判 if (sm[i] - sm[i - 1] >= R) {ans = max(ans, p[i] * 1.0);++l, ++r; continue;}while (l <= n && sm[l] - sm[i - 1] < L) ++l;while (r + 1 <= n && sm[r + 1] - sm[i - 1] <= R) ++r;if (l > n) break;db pl = 1.0 * L / (sl[l - 1] - sl[i - 1] + (L - (sm[l - 1] - sm[i - 1])) * 1.0 / p[l]);db pr = 1.0 * R / (sl[r] - sl[i - 1] + (R - (sm[r] - sm[i - 1])) * 1.0 / p[r + 1]);if (r > n) pr = 0.0;ans = max(ans, max(pl, pr));}// 算选的都是整块的情况db l = 0.0, r = 0.0;for (int i = 1; i <= n; ++i) r = max(r, p[i] * 1.0);while (r - l >= 1e-7) {db mid = (l + r) / 2;if (chk(mid)) l = mid, ans = max(ans, mid);else r = mid;}
}
int main() {
// freopen("connect2.in", "r", stdin);scanf("%d%lld%lld", &n, &L, &R);for (int i = 1; i <= n; ++i) scanf("%d", len + i);for (int i = 1; i <= n; ++i) scanf("%d", p + i);solve();reverse(len + 1, len + n + 1);reverse(p + 1, p + n + 1);solve();printf("%.10lf\n", ans);return 0;
}
T3 洛谷 U490735 习惯孤独
一道这辈子做的最沟施的树形 dpdpdp,光改题改了我 5h5h5h,代码 + 注释 + 调试代码一共 272 行,写篇题解发泄一下,希望这题没白改。
一、暴力思路
直接 dfsdfsdfs,枚举第 iii 次删一条边,然后判断删去这条边后形成的两个连通块是否符合要求,
如果符合就直接 dfsdfsdfs 下去,不符合就不管。
理论复杂度是 O(nk)O(n^k)O(nk),但是远远跑不满,因为不合法的删边相当多。
考场上有一个人拿这得了 70pts70pts70pts,再次警醒写暴力的重要性。但我懒得写了
二、正解
树上统计方案数一般就是树形 dpdpdp,树形 dpdpdp 的精髓就在于分类讨论。
首先转化一下 aia_iai 的含义,aia_iai 表示第 iii 刀要砍下的子树大小,即 ai=ai−1−aia_i = a_{i - 1} - a_iai=ai−1−ai。
(一) 在以 uuu 为根的子树内切割
看见 k≤6k \leq 6k≤6,所以可以用状态压缩表示切割状态。
设 fu,sf_{u, s}fu,s 表示以 uuu 为根节点的子树,切割状态为 sss 时的方案数。
fu,sf_{u, s}fu,s 一共有两部分构成:一是 uuu 的子节点,二是当它自己被划分到一个连通块时也有贡献。
上述情况如下图:
- 子节点 vvv 的贡献(我们把这部分贡献记为 gu,sg_{u,s}gu,s):
- uuu 自己也在一个连通块中:
对于第一种情况,fu,sf_{u, s}fu,s 的答案就是若干个子树的方案数的乘积,但是要注意,每个子树的状态 SvS_vSv 并起来要等于 sss,且全部 &\&& 起来要为 000,因为不可能砍同一刀;
对于第二种情况,要先判断 uuu 所在的连通块的大小是否符合某一个切割要求,即判断以 uuu 为根的子树剩下的大小是否和某个 aia_iai 相同。需要注意的是,uuu 所在的连通块必须比它子树中的连通块的切割时间要晚。下图就是一个不合法的情况:
(二) 在以 uuu 为根的子树外切割
在 uuu 子树以外的部分的贡献由三部分组成:
-
uuu 的兄弟节点划分成的连通块:
-
包含父亲节点的连通块(下面展示一种情况,当然 fafafa 还可能被包含在"上面连着其他部分"的某个连通块):
3.在【"上面连着其他部分"且不包含以 fafafa 为根的子树】的连通块:
因此设 hu,vh_{u, v}hu,v 表示在 uuu 子树以外的部分的方案数。
考虑这部分怎么求:
考虑换根 dpdpdp,那么这部分就可以用 ffaf_{fa}ffa 减去 fuf_ufu 的那一部分贡献来求,但是这一部分贡献并不好抽离出来(但是为什么我也不太清楚,同机房大佬说是因为这方案数计算的时候实际上是卷积的过程,我也完全不知道是啥),所以考虑正向求解。
- 首先是兄弟节点,这部分何以用前后缀来求:prei,spre_{i, s}prei,s 表示 uuu 的前 iii 个兄弟节点的切割状态并集为 sss 时的方案数,sufi,ssuf_{i, s}sufi,s 表示第 iii 个兄弟节点到最后一个兄弟节点的切割状态并集为 sss 的方案数,那么这部分贡献就是 hu,s1∣s2=prei−1,s1×sufi+1,s2h_{u, s1 | s2} = pre_{i-1, s1} \times suf_{i + 1, s2}hu,s1∣s2=prei−1,s1×sufi+1,s2(s1&s2=0s1 \& s2 = 0s1&s2=0,uuu 就是第 iii 个兄弟节点);
- 其次是父亲节点的贡献:这部分求法和在【求 fuf_ufu 中 uuu 本身属于一个连通块的贡献】的方法一样;
- 最后是 fafafa 以外的贡献:fafafa 以外的贡献实际上就是 hfah_{fa}hfa,将它与 huh_uhu 合并就行。
最后答案是 ∑i=1n∑s=02k−1gi,s×hi,(2k−1)⊕s\sum_{i = 1}^{n} \sum_{s = 0} ^ {2^k - 1} g_{i, s} \times h_{i, (2^k - 1) \oplus s}∑i=1n∑s=02k−1gi,s×hi,(2k−1)⊕s,但是还要除以 aka_kak,因为我们是钦定 iii 在最后一个连通块中,会有 aka_kak 个点记录同一种情况。
代码如下:
#include <bits/stdc++.h>using namespace std;typedef long long ll;const int maxn = 5e3 + 7;
const int maxs = (1 << 6) + 7;
const int mod = 998244353;// 注: 下面所说的 "子节点的子树" 指的是 "【以 u 的子节点】为根的子树"int n, m, ms, a[10];
vector<int> e[maxn];int siz[maxn];ll f[maxn][maxs]; // f[u][s] 表示以 u 为根节点的子树(且【包含 u 节点】)切的状态为 s 时的方案数
ll g[maxn][maxs]; // g[u][s] 表示以 u 为根节点的子树(且【不包含 u 节点】)切的状态为 s 时的方案数
ll tmp[maxs];void dfs1(int u, int fa) {// 没切割也算一种方案 siz[u] = f[u][0] = 1;int fid = -1; // 记录父节点在 e[u] 中的下标// f[u] 由两部分组成, 一个是其子节点子树的贡献(其实就是 g[u]), 一个是其本身被包含到一个连通块的贡献// 下面这部分求的是其 g[u] 的部分for (int i = 1; i <= e[u].size(); ++i) {int v = e[u][i - 1];if (v == fa) {fid = i - 1; continue;}dfs1(v, u), siz[u] += siz[v];// 记录前 i - 1 棵子节点子树【不同切割状态下】的贡献// 等会用于合并for (int s = 0; s <= ms; ++s)tmp[s] = f[u][s], f[u][s] = 0;// 枚举前 i - 1 棵子节点子树切割状态的【并集】 // 并将其与第 i 棵合并for (int si = 0; si <= ms; ++si) {int sj = ms ^ si; // 由于不可能砍同一刀, 所以 si & sk 必须等于 0 for (int sk = sj; ; sk = (sk - 1) & sj) {f[u][si | sk] = (f[u][si | sk] + tmp[si] * f[v][sk] % mod) % mod;if (!sk) break;}}}// 父边没用就删了 if (fid != -1) e[u].erase(e[u].begin() + fid);// 记录子节点的贡献for (int s = 0; s <= ms; ++s)g[u][s] = f[u][s];// 现在计算 u 本身被包含到一个连通块的贡献for (int si = 0; si <= ms; ++si) { // 枚举其所有子节点子树的切割状态的【并集】 int cut = 0; // 记录子节点子树中被砍去的大小for (int j = 1; j <= m; ++j)if ((si >> (j - 1)) & 1) cut += a[j]; int rest = siz[u] - cut; // 剩下的节点数 // 枚举 u 可能会被哪些连通块包含// 这里必须倒序枚举, 因为【包含 u 的连通块】必须比【子节点子树的连通块】切得晚 for (int j = m; j; --j) {if ((si >> (j - 1)) & 1) break; // 不能继续枚举比子节点子树中【最晚的连通块】更早的连通块 if (rest == a[j]) f[u][si | (1 << (j - 1))] = (f[u][si | (1 << (j - 1))] + g[u][si]) % mod;}}
}ll pre[maxn][maxs]; // pre[i][s] 表示 u 的前 i 个子树的切割状态为 s 时的方案数
ll suf[maxn][maxs]; // suf[i][s] 表示 u 的第 i 个子节点到最后一个子节点的切割状态为 s 时的方案数
ll h[maxn][maxs]; // h[u][s] 表示除【以 u 为根节点的子树】以外其他部分的贡献
void dfs2(int u) {for (int i = 0; i <= e[u].size() + 1; ++i)for (int s = 0; s <= ms; ++s)pre[i][s] = suf[i][s] = 0;pre[0][0] = suf[e[u].size() + 1][0] = 1;for (int i = 1; i <= e[u].size(); ++i) {int v = e[u][i - 1];for (int si = 0; si <= ms; ++si) { // 枚举前 i - 1 个子节点子树的切割状态并集 int sj = si ^ ms;for (int sk = sj; ; sk = (sk - 1) & sj) { // 枚举第 i 个子节点子树切割状态 pre[i][si | sk] = (pre[i][si | sk] + pre[i - 1][si] * f[v][sk] % mod) % mod;if (!sk) break;}}}for (int i = e[u].size(); i; --i) {int v = e[u][i - 1];for (int si = 0; si <= ms; ++si) {int sj = si ^ ms;for (int sk = sj; ; sk = (sk - 1) & sj) {suf[i][si | sk] = (suf[i][si | sk] + suf[i + 1][si] * f[v][sk] % mod) % mod;if (!sk) break;}}}// h[v][s] 贡献来自三部分for (int i = 1; i <= e[u].size(); ++i) {int v = e[u][i - 1];// 1. v 的兄弟节点for (int si = 0; si <= ms; ++si) { // 枚举前缀兄弟节点的切割状态的【并集】 int sj = ms ^ si;for (int sk = sj; ; sk = (sk - 1) & sj) { // 枚举后缀兄弟节点的切割状态的【并集】h[v][si | sk] = (h[v][si | sk] + pre[i - 1][si] * suf[i + 1][sk] % mod) % mod;if (!sk) break;}}// 2. v 的父亲节点以外的for (int s = 0; s <= ms; ++s)tmp[s] = h[v][s], h[v][s] = 0;for (int si = 0; si <= ms; ++si) { // 枚举【除【以 u 为根节点的子树】以外】的部分的切割状态并集 int sj = si ^ ms;for (int sk = sj; ; sk = (sk - 1) & sj) {h[v][si | sk] = (h[v][si | sk] + h[u][si] * tmp[sk] % mod) % mod;if (!sk) break;}}// 3. v 的父亲节点(就是把 (u, v) 断开后, 产生的包含 u 的连通块的贡献) for (int s = 0; s <= ms; ++s) tmp[s] = h[v][s];// 这里枚举的实际上是: 整棵树中【除以 u 为根的子树以外部分】和【v 的兄弟节点子树】的切割状态 for (int sj = 0; sj <= ms; ++sj) {int cut = 0;for (int j = 1; j <= m; ++j)if ((sj >> (j - 1)) & 1) cut += a[j];int rest = n - siz[v] - cut;for (int j = m; j; --j) {if ((sj >> (j - 1)) & 1) break;if (rest == a[j]) h[v][sj | (1 << (j - 1))] = (h[v][sj | (1 << (j - 1))] + tmp[sj]) % mod;}}}for (int v : e[u]) dfs2(v);
}
ll qpow(ll x, ll y) {ll res = 1;for (; y; y >>= 1, x = x * x % mod)if (y & 1) res = res * x % mod;return res;
}
int main() {scanf("%d", &n);for (int i = 1, u, v; i < n; ++i) {scanf("%d%d", &u, &v);e[u].push_back(v);e[v].push_back(u);}scanf("%d", &m), ms = (1 << m) - 1;for (int i = 1; i <= m; ++i) scanf("%d", a + i);a[0] = n;for (int i = m + 1; i; --i)a[i] = a[i - 1] - a[i]; // a[i] 表示第 i 次切割时需要切去的子树大小// a[k] 还要保留下来, 等会统计答案时还要用dfs1(1, 0);h[1][0] = 1;dfs2(1);ll ans = 0;for (int i = 1; i <= n; ++i) {for (int s = 0; s <= ms; ++s) tmp[s] = 0;for (int si = 0; si <= ms; ++si) {int sj = ms ^ si;for (int sk = sj; ; sk = (sk - 1) & sj) {tmp[si | sk] = (tmp[si | sk] + g[i][si] * h[i][sk] % mod) % mod;if (!sk) break;}}ans = (ans + tmp[ms]) % mod;}// 由于在统计答案时, 我们钦定 i 留在最后一个块中// 所以会有 a[k] 个点记录的是同一个方案// 所以答案要除以 a[k]printf("%lld\n", ans * qpow(a[m + 1], mod - 2) % mod);return 0;
}
时间复杂度是 O(n×22k)O(n \times 2^{2k})O(n×22k).
T4 车站
是最小树形图,不会先不改。