T1 Rank of Tetris
挺好的拓扑排序+并查集按秩合并的题。
转化题意:
给定 N N N个点和 M M M个关系,可以使 = 、 > 或 < =、>或< =、>或<的关系,求能否唯一确定这 N N N个点的大小关系。
解题思路:
看到 = = =可以用并查集来处理把相等关系的点都缩一个点,然后就是判断这个缩点之后的图是否是一个 D A G DAG DAG,若不是 D A G DAG DAG则说明是 d e e p deep deep,否则再判断是否是 d a r k dark dark,如何判断呢?
用一个 d i s dis dis数组记录拓扑排序出的点距离起始点的层次关系,若存在两个缩点的 d i s dis dis相同,则说明这两个点关系不明确,或者存在某一个缩点它没有出边和入边,且它的秩小于总点数 N N N,说明这个集合被孤立出去,集合内的点关系也是不明确的。当然一开始得先离线处理缩点,不然万一输入次序一变化,加的边就不对了。
c o d e code code
//并查集+拓扑排序
#include<bits/stdc++.h>
using namespace std;
const int maxn = 20005;
const int N = 10005;
struct path {
int fr, to;
char ss[2];
} tt[maxn];
vector<int>aa[N]; //模拟邻接表
int n, m, num;
int father[N];
int idx[N]; //入度
void init() {
for (int i = 0; i < n; i++)
father[i] = i;
}
int find(int x) {
while (x != father[x]) x = father[x];
return x;
}
void unin(int a, int b) {
father[b] = a;
}
int main() {
while (scanf("%d%d", &n, &m) != EOF) {
init();
num = n;
for (int i = 0; i < m; i++) {
scanf("%d%s%d", &tt[i].fr, tt[i].ss, &tt[i].to);
if (tt[i].ss[0] == '=') {
int x = find(tt[i].fr);
int y = find(tt[i].to);
if (x != y) {
unin(x, y);
num--;
}
}
}
bool err = 0, fight = 0;
memset(idx, 0, sizeof(int) * (n + 1));
for (int i = 0; i < n; i++)
aa[i].clear();
for (int i = 0; i < m; i++) {
if (tt[i].ss[0] != '=') {
int x = find(tt[i].fr);
int y = find(tt[i].to);
if (x == y) {
err = 1; //有矛盾
break;
}
//构建拓扑图
if (tt[i].ss[0] == '>') {
aa[x].push_back(y);
idx[y]++;
} else {
aa[y].push_back(x);
idx[x]++; //记录入读的情况
}
}
}
if (err) {
printf("fantasy\n");
continue;
}
queue<int>q;
//找出所有入度为0的点。
for (int i = 0; i < n; i++)
if (idx[i] == 0 && i == find(i))
q.push(i);
while (!q.empty()) {
if (q.size() > 1) {
fight = 1;
}
int gt = q.front();
q.pop();
num--;
vector<int>::iterator ww;
for (ww = aa[gt].begin(); ww != aa[gt].end(); ww++) {
idx[*ww]--;
if (idx[*ww] == 0)
q.push(*ww);
}
}
if (num > 0) { //成环
printf("fantasy\n");
continue;
}
if (!fight) printf("deep\n");
else printf("dark\n");
}
return 0;
}
T2 P8945 Inferno
入只因zsj气死我了!!!(╯‵□′)╯炸弹!•••*~●
这道题贪心+前缀和就可解决,单调队列没用到。
转化题意:
给一个仅包含 − 1 , 0 , 1 -1,0,1 −1,0,1的序列 a 。 a。 a。在为 0 0 0的位置中选 k k k个1更改为 1 , 1, 1,其余更改为 − 1 -1 −1,使得最大子段和最大,求最大子段和的最大值。
解题思路:
枚举左端点 − 1 -1 −1,设为 i i i。考虑右端点 j j j。
记原序列前缀和为 p r i pr_i pri。
我们考虑定义 c c c表示 [ i , j ] [i,j] [i,j]中 0 0 0的数量。我们尽可能填 1 1 1,再填 − 1 -1 −1,则对 c c c的大小分类讨论。
- c ≤ k , c \le k, c≤k,这段的字段和为 p r j − p r i + c pr_j-pr_i+c prj−pri+c
- c > k , c>k, c>k,这段的字段和为 p r j − p r i + 2 k − c pr_j-pr_i+2k-c prj−pri+2k−c
我们定义 p o s i pos_i posi为第 i i i个 0 0 0的位置, l i l_i li为 i i i左方(不含 i i i)的最后一个 0 0 0是第几个 0 0 0。则当 j ∈ ( i , p o s l i + k + 1 ) j\in(i,pos_{l_i+k+1}) j∈(i,posli+k+1)时 c ≤ k , j ∈ [ p o s l i + k + 1 , n ] c \le k,j\in[pos_{l_i+k+1},n] c≤k,j∈[posli+k+1,n]时 c > k c>k c>k。
考虑将 c c c从式子中消掉。定义 p − 1 , i p_{-1,i} p−1,i的后缀 max \max max。对于前者,为一段区间求 max \max max,且左右端点单调不减,维护 p l , j p_{l,j} pl,j的单调队列就行。
c o d e code code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = x * 10 + ch - '0', ch = getchar();
return x * f;
}
void write(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
const int N = 1e7 + 10;
int n, k;
int a[N];
int bel[N];
int cnt, pos[N], ans;
int pm[N], sm[N], p0[N], p1[N];
struct node {
int v, p;
} stk[N];
int hd = 1, tl;
int main() {
n = read(), k = read();
for (int i = 1, las = 0; i <= n; i++) {
a[i] = read();
bel[i] = las;
if (a[i]) {
p0[i] = p0[i - 1] + a[i];
p1[i] = p1[i - 1] + a[i];
} else {
p0[i] = p0[i - 1] - 1;
p1[i] = p1[i - 1] + 1;
pos[++cnt] = i, las = cnt;
}
}
pm[n + 1] = sm[n + 1] = -2e9;
for (int i = n; i >= 1; i--)
pm[i] = max(pm[i + 1], p1[i]),
sm[i] = max(sm[i + 1], p0[i]);
for (int i = 0, lp = 1; i <= n; i++) {
while (hd <= tl && stk[hd].p < i) hd++;
int id = bel[i] + k + 1;
if (id > cnt) ans = max(ans, pm[i + 1] - p1[i]);
else {
int np = pos[id];
for (; lp <= np - 1; lp++) {
while (hd <= tl && stk[tl].v < p1[lp]) tl--;
stk[++tl] = {p1[lp], lp};
}
ans = max({ans, stk[hd].v - p1[i], sm[np] - p0[i] + 2 * k});
}
}
write(ans);
return 0;
}
T3 CF620E 【New Year Tree】
这道题其实是线段树维护子树的问题,会用到状压+线段树+子树的dfs序。
首先我们要用到一个叫 d f s dfs dfs序的概念。
它本质上就是一棵树的先序遍历,所谓先序遍历就是先遍历根,然后遍历左子节点,最后遍历右子节点。我们需要把dfs序存在pos数组中,并把每个节点第一次遍历到的时间点和第二次遍历到的时间点存到in和out数组中,这样就成功地把一棵树转换为了线性结构。对一棵子树进行操作时,只需对这棵子树的根节点两次遍历到的时间戳中间的部分进行操作即可。
求dfs序的代码:
void dfs(ll x,ll fa){
tim++;
in[x]=tim;
pos[tim]=x;
for(ll i=head[x];i;i=edge[i].next){
ll y=edge[i].v;
if(y==fa) continue;
dfs(y,x);
}
out[x]=tim;
}
然后我们就可以用dfs序,也就是pos数组对线段树进行操作了,不过需要用到状态压缩,要把颜色压缩成二进制数到线段树中,所以要开long long。
接下来基本上都是线段树区间修改,区间查询的模板了。需要注意的是,查询出来的值是一个经过状压后的数,我们需要把它分解。这里可以借鉴树状数组的思想,即每次减去一个 l o w b i t lowbit lowbit(一棵树上有数值的节点的最低位,不会的话可以先去学习一下树状数组,这里不再过多赘述)再让 a n s + + ans++ ans++,因为状压后只有 0 0 0和 1 1 1,有值的话一定是 1 。 a n s 1。ans 1。ans就是最后的答案。
c o d e code code
#include<bits/stdc++.h>
#define N 400010
#define ll long long
using namespace std;
struct node { //用前向星找dfs序
ll v, next;
} edge[N << 2];
ll head[N], tot, tim;
ll in[N], out[N], pos[N];
ll a[N];
ll ans[N << 2], lazy[N << 2]; //线段树开4倍空间
void dfs(ll x, ll fa) {
tim++;
in[x] = tim;
pos[tim] = x;
for (ll i = head[x]; i; i = edge[i].next) {
ll y = edge[i].v;
if (y == fa) continue;
dfs(y, x);
}
out[x] = tim;
}
void add(ll x, ll y) {
tot++;
edge[tot].v = y;
edge[tot].next = head[x];
head[x] = tot;
}
void pushup(ll rt) {
ans[rt] = ans[rt << 1] | ans[rt << 1 | 1]; //状压,所以是或运算
}
void pushdown(ll rt) {
if (lazy[rt] != 0) {
lazy[rt << 1] = lazy[rt];
lazy[rt << 1 | 1] = lazy[rt];
ans[rt << 1] = lazy[rt];
ans[rt << 1 | 1] = lazy[rt];
lazy[rt] = 0;
}
}
void build(ll l, ll r, ll rt) {
if (l == r) {
ans[rt] = 1ll << (a[pos[l]]);
return;
}
ll mid = (l + r) >> 1;
build(l, mid, rt << 1);
build(mid + 1, r, rt << 1 | 1);
pushup(rt);
}
void update(ll L, ll R, ll x, ll l, ll r, ll rt) {
if (L <= l && r <= R) {
ans[rt] = 1ll << x; //存的是二进制数,左移x位代表颜色,刚开始接触的话如果不懂多做题就能慢慢理解了
lazy[rt] = 1ll << x; //1ll是强制类型转换成long long的意思,一定要有,当时我就在这里卡了好久
return;
}
pushdown(rt);
ll mid = (l + r) >> 1;
if (L <= mid) update(L, R, x, l, mid, rt << 1);
if (R > mid) update(L, R, x, mid + 1, r, rt << 1 | 1);
pushup(rt);
}
ll query(ll L, ll R, ll l, ll r, ll rt) {
if (L <= l && r <= R)
return ans[rt];
pushdown(rt);
ll mid = (l + r) >> 1;
ll res = 0;
if (L <= mid) res |= query(L, R, l, mid, rt << 1);
if (R > mid) res |= query(L, R, mid + 1, r, rt << 1 | 1);
return res;
}
ll lowbit(ll k) {
return k & (-k);
}
ll getsum(ll x) {
ll ans = 0;
for (ll i = x; i > 0; i -= lowbit(i))
ans++;
return ans;
}
int main() {
ll n, m, p, v, c;
scanf("%lld%lld", &n, &m);
for (ll i = 1; i <= n; i++)
scanf("%lld", &a[i]);
for (ll i = 1; i < n; i++) {
ll x, y;
scanf("%lld%lld", &x, &y);
add(x, y);
add(y, x);
}
dfs(1, 0);
build(1, n, 1);
for (ll i = 1; i <= m; i++) {
scanf("%lld", &p);
if (p == 1) {
scanf("%lld%lld", &v, &c);
update(in[v], out[v], c, 1, n, 1);
} else {
scanf("%lld", &v);
ll num = query(in[v], out[v], 1, n, 1);
printf("%lld\n", getsum(num));
}
}
return 0;
}
T4 CF673E Levels and Regions
本题标签为前缀和+期望+斜率优化(挺喜欢做完看看标签的)
转化题意:
将 n n n个数字连续分为 k k k段,要求将所有数字操作一遍,问期望操作次数最小。
代价的计算:
设当前段到操作第
i
i
i个数字,则操作一次有
t
i
s
u
m
i
\frac{t_i}{sum_i}
sumiti的概率,到下一个数字,否则留在这个数字。其中
s
u
m
i
sum_i
sumi表示这段数字到第
i
i
i个的前缀和,
t
i
t_i
ti表示有
i
i
i个数字。
解题思路:
从只分成一段开始入手,根据期望的套路,从后往前开始 d p dp dp,则 f i = p ∗ f i + 1 + ( 1 − p ) ∗ f i + 1 f_i=p*f_{i+1}+(1-p)*f_i+1 fi=p∗fi+1+(1−p)∗fi+1其中 p = t i s u m i p= \frac{t_i}{sum_i} p=sumiti
接下来设 f l , r f_{l,r} fl,r表示 [ l , r ] [l,r] [l,r]这段的期望,我们可以表示:
f l , r = ∑ i = l r ∑ j = l i t j = ∑ i = l r sum i − sum l − 1 t i = ∑ i = l r sum i t i − sum l − 1 ∗ ∑ i = l r 1 t i \begin{aligned} f_{l, r} & =\sum_{i=l}^{r} \frac{\sum_{j=l}^{i}}{t_{j}} \\ & =\sum_{i=l}^{r} \frac{\text { sum }_{i}-\text { sum }_{l-1}}{t_{i}} \\ & =\sum_{i=l}^{r} \frac{\operatorname{sum}_{i}}{t_{i}}-\text { sum }_{l-1} * \sum_{i=l}^{r} \frac{1}{t_{i}} \end{aligned} fl,r=i=l∑rtj∑j=li=i=l∑rti sum i− sum l−1=i=l∑rtisumi− sum l−1∗i=l∑rti1
设 A i = ∑ s u m i t i , B = ∑ 1 t i A_i=\sum {\frac{sum_i}{t_i}},B=\sum \frac{1}{t_i} Ai=∑tisumi,B=∑ti1那么 f l , r f_{l,r} fl,r就是 min { A r − A l − 1 − s u m l − 1 ∗ ( B r − B r − 1 ) } \min {\left \{ A_r-A_{l-1}-sum_{l-1}*(B_r-B_{r-1}) \right \} } min{Ar−Al−1−suml−1∗(Br−Br−1)}
设 d p i , k dp_{i,k} dpi,k表示前 i i i个数分成 k k k段的最小期望,那么有:
d p i , k = min ( d p j , k − 1 + A i − A j − s u m j ∗ ( B i − B j ) ) dp_{i,k}=\min(dp_{j,k-1}+A_i-A_j-sum_j*(B_i-B_j)) dpi,k=min(dpj,k−1+Ai−Aj−sumj∗(Bi−Bj))
其实这是斜率优化的式子,我们可以设 k < j k<j k<j且 j j j比 k k k更优,可以得到:
( g j − A j + sum j ∗ B j ) − ( g k − A k + sum k ∗ B k ) sum j − sum k < B i \frac{\left(g_{j}-A_{j}+\operatorname{sum}_{j} * B_{j}\right)-\left(g_{k}-A_{k}+\operatorname{sum}_{k} * B_{k}\right)}{\operatorname{sum}_{j}-\operatorname{sum}_{k}}<B_{i} sumj−sumk(gj−Aj+sumj∗Bj)−(gk−Ak+sumk∗Bk)<Bi
g i g_i gi表示分成 k − 1 k-1 k−1段时处理出来的 d p dp dp数组。
c o d e code code
#include<bits/stdc++.h>
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef double db;
typedef pair<db, db> pdd;
const int N = 2e5 + 10;
int n, K;
db A[N], B[N], S[N], f[N], g[N];
pdd q[N];
int read() {
int ret = 0;
char c = getchar();
while (!isdigit(c)) {
c = getchar();
}
while (isdigit(c)) {
ret = ret * 10 + (c ^ 48);
c = getchar();
}
return ret;
}
void init() {
n = read();
K = read();
for (int i = 1; i <= n; ++i) {
db x = read();
S[i] = S[i - 1] + x;
f[i] = g[i] = A[i] = A[i - 1] + S[i] / x;
B[i] = B[i - 1] + 1.0 / x;
//printf("%lf %lf %lf\n",A[i],B[i],S[i]);
}
}
db cross(pdd A, pdd B) {
return (A.se - B.se) / (A.fi - B.fi);
}
void solve(int d) {
int l = 0, r = 0;
for (int i = 1; i <= n; ++i) {
while (r - l && cross(q[l], q[l + 1]) < B[i]) ++l;
f[i] = q[l].se + A[i] - q[l].fi * B[i];
pdd tmp = mkp(S[i], g[i] - A[i] + S[i] * B[i]);
//printf("%lf %lf %lf %lf\n",A[i],B[i],q[l].fi,q[l].se);
while (r - l && cross(q[r], tmp) < cross(q[r - 1], tmp)) --r;
q[++r] = tmp;
}
for (int i = 1; i <= n; ++i) g[i] = f[i];
}
int main() {
init();
for (int i = 2; i <= K; ++i)
solve(i);
printf("%0.2lf\n", f[n]);
return 0;
}
总结
今天的题数据结构偏多,dp的题也非常巧妙,总结的话,还是打暴力。
1103

被折叠的 条评论
为什么被折叠?



