2025-11-12~13 hetao1733837的刷题记录
刷题背景
这是一个悲伤的故事……

11-12
P3206 [HNOI2010] 城市建设
原题链接:[P3206 [HNOI2010] 城市建设]([P3206 HNOI2010] 城市建设 - 洛谷)
分析
一眼最小生成树,但是,掺进了修改……不同于[P1340](P1340 兽径管理 - 洛谷)的是,这题不是加边而是改边权,然后就评紫了……其实,在最开始学$CDQ$分治(虽然没学会吧……)的时候,我就意识到了其代码和线段树的相似性。所以说,$CDQ$分治的递归树就是一棵线段树。普通$CDQ$分治是在考虑线段树左右儿子的关系,而本题则需要考虑儿子父亲之间的关系。也就是说,我们在调用$CDQ(l,r)$时,需要使图与区间产生一个关联(我是这么理解的······)
具体的实现如下:
若我们正在构造$(l,r)$这段区间的最小生成树边集,并且我们已知它父亲最小生成树的边集。我们将在$(l,r)$这段区间中发生变化的边赋值为$+\infty$和$-\infty$,各跑一次$Kruskal$,求最小生成树里的边。
对于一条边,
1.如果被赋值为$+\infty$,而它未出现在树中,则证明它不可能出现在$(l,r)$这些询问的最小生成树中。所以,我们仅仅在$(l,r)$的边集中加入最小生成树的边。
2.如果最小生成树里所有被修改的边权都被赋成了$-\infty$,而它出现在树中,则证明它一定会出现$(l,r)$这段的区间的最小生成树当中。这样的话,我们就可以使用并查集将这些边对应的点缩起来,并且将答案加上这些边的边权。
于是,我们得到$(l,r)$这段区间的边集构造出来了。用这些边求出最小生成树和直接求原图的最小生成树是等价的。
易于证明:复杂度是$O(nlong^2n)$的。
正解
我为何$MLE$,$WA$掉了???
P14426 [JOISC 2014] 稻草人 / Scarecrows
原题链接:[P14426 [JOISC 2014] 稻草人 / Scarecrows]([P14426 JOISC 2014] 稻草人 / Scarecrows - 洛谷)
我吃饱了/ll
分析
好吧,我真的吃饱了。容易想到的是存在$(i,j)$使得$x_i< x_j,y_i< y_j$,但是,我们还要控制“烟堆的内部(不包括边界)不能有神社”,这也成为了本题的难点……我去看题解了,能学会吗?
考虑到我是天然的二维偏序,维护额外信息再加一维即可,即需要满足$\nexists k$,使得$x_i < x_k < x_j$且$y_i < y_k < y_j$。考虑分治,我们把$x$轴分为左右两部分,会发现在左半部分,一个点可以统计答案当且仅当它不为西南角,所以,左半区间的y随x的增加而减少($k<0$),同理右半子区间y随x的增加而增加($k>0$),那么,可以开单调栈维护,每次查询我们需要找到比前一个$y$大,比当前$y$小,这就是二分(此处默认通过对$x$的排序消掉了一维)。好吧,我需要继续理解……
正解
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 200005;
int n, stk1[N], stk2[N];
int tp1, tp2;
struct node{
int x, y, id;
bool operator<(const node &k) const{
return x < k.x;
}
}a[N], b[N];
int ans;
void CDQ(int l, int r){
if (l == r)
return ;
int mid = (l + r) >> 1;
CDQ(l, mid);
CDQ(mid + 1, r);
int i = l, j = mid + 1, cnt = 0;
while (i <= mid && j <= r){
if (a[i].y < a[j].y){
b[++cnt] = {a[i].x, a[i].y, 0};
i++;
}
else{
b[++cnt] = {a[j].x, a[j].y, 1};
j++;
}
}
while (i <= mid){
b[++cnt] = {a[i].x, a[i].y, 0};
i++;
}
while (j <= r){
b[++cnt] = {a[j].x, a[j].y, 1};
j++;
}
tp1 = 0;
tp2 = 0;
for (int i = 1; i <= cnt; i++){
if (!b[i].id){
while (tp1 && b[stk1[tp1]].x < b[i].x){
tp1--;
}
stk1[++tp1] = i;
}
else{
while (tp2 && b[stk2[tp2]].x > b[i].x){
tp2--;
}
if (!tp2){
ans += tp1;
}
else{
int s = 1, t = tp1, pos = tp1 + 1;
while (s <= t){
int mid = (s + t) >> 1;
if (b[stk1[mid]].y > b[stk2[tp2]].y){
t = mid - 1;
pos = mid;
}
else{
s = mid + 1;
}
}
ans += tp1 - pos + 1;
}
stk2[++tp2] = i;
}
}
for (int i = l; i <= r; i++){
a[i] = b[i - l + 1];
}
}
signed main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i].x >> a[i].y;
}
sort(a + 1, a + n + 1);
CDQ(1, n);
cout << ans;
}
记得开$long$ $ long$哦!
11-13
不知为何,机房都在打$OIer$教练模拟器……连$lz$都在打……
刷题背景
退竞了······$tboj$号你们想登就登吧,对$OI$的新鲜感过了,账号也抽了很多卡了,我也不想学了,送给大家了······这个事情我也想了很久,非常舍不得。最终,还是决定不玩了。学了这么久还是没有什么进步,我对自己太失望了/ll
账号:$KfcCrazyThursday$
密码:$Vwo50rmb$
P11362 [NOIP2024] 遗失的赋值
原题链接:[P11362 [NOIP2024] 遗失的赋值]([P11362 NOIP2024] 遗失的赋值 - 洛谷)
分析
比较傻了,就是乘法原理套加法原理,但是,感觉最近睡眠有点少,脑子比较糊(我在找借口),所以没有直接切掉……糖丸了,$qpow()$初始化写res=0也是有了……
正解
#include <bits/stdc++.h>
#define int long long
#define mod 1000000007
using namespace std;
int qpow(int x, int y){
int ans = 1;
while (y){
if (y & 1)
ans = ans * x % mod;
x = x * x % mod;
y >>= 1;
}
return ans;
}
int n, m, v, ans, T;
map<int, int> mp;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> T;
while (T--){
cin >> n >> m >> v;
mp.clear();
ans = 1;
for (int i = 1; i <= m; i++){
int c, d;
cin >> c >> d;
if (mp.find(c) != mp.end() && mp[c] != d)
ans = 0;
else
mp[c] = d;
}
if (!ans){
cout << 0 << '\n';
continue;
}
auto pre = mp.begin(), nxt = mp.begin();
ans = qpow(v, ((*nxt).first - 1) * 2);
++nxt;
for ( ; nxt != mp.end(); ++pre, ++nxt){
int p = (*pre).first, q = (*nxt).first;
ans = ans * (qpow(v, (q - p) * 2) - qpow(v, q - p - 1) * (v - 1) % mod + mod) % mod;
}
ans = ans * qpow(v, (n - (*pre).first) * 2) % mod;
cout << ans << '\n';
}
}
好像并未理解,脑子还是糊……
P7961 [NOIP2021] 数列
原题链接:[P7961 [NOIP2021] 数列]([P7961 NOIP2021] 数列 - 洛谷)
分析
想到了找一些规律,就比如说$(10)_2+(10)_2=(100)_2$这样在同一位上的$1$的个数$cnt$,会转化为$\left\lfloor\dfrac{cnt}{2}\right\rfloor$个更高位的$1$,和$cnt\mod 2$个当前位置上的$1$。呃,然后比一下$k$,然后加法原理和乘法原理往上套……思路大概长这样。对吗?容我看一眼题解……
woc,咋是$DP$?好吧,重新稍考一下……对于和$k$比的部分,显然是__builtin_popcount()吧……$O(1)$显然很有性价比。那么,就是状态了……由于牵扯进位,所以不妨从低位往高位$DP$,设$dp_{i,j,k,p}$表示讨论了$S$从低到高的前$i$位,已经确定了$j$个序列$a$中的元素,$S$从低到高前$i$位中有$k$个$1$,要向当前讨论位的下一位进位$p$。
由之前瞎想的规律得出:$dp_{i,j,k,p}$可以更新$dp_{i+1,j+t,k+(t+p)\mod2,\left\lfloor\dfrac{cnt}{2}\right\rfloor}$,由于乘积之和的形式满足乘法分配律,所以不难想到$dp_{i,j,k,p}$对新状态的贡献应该是$dp_{i,j,k,p}\times v_{i}^{v}\times \dbinom{n-j}{t}$,对于初始化$dp_{0,0,0,0}=1$。
答案统计,前三维按照本身统计,最后一维随意。
好吧,我承认这题确实难……
正解
#include <bits/stdc++.h>
#define mod 998244353
using namespace std;
const int N = 35, M = 105;
int n, m, K;
long long ans, v[M], dp[M][N][N][N], C[N][N], P[M][N];
int main(){
// freopen("sequence.in", "r", stdin);
// freopen("sequence.out", "w", stdout);
for (int i = 0; i < N; i++)
C[i][0] = 1;
for (int i = 1; i < N; i++)
for (int j = 1; j <= i; j++)
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
cin >> n >> m >> K;
for (int i = 0; i <= m; i++){
cin >> v[i];
P[i][0] = 1;
for (int j = 1; j <= n; j++)
P[i][j] = P[i][j - 1] * v[i] % mod;
}
dp[0][0][0][0] = 1;
for (int i = 0; i <= m; i++){
for (int j = 0; j <= n; j++){
for (int k = 0; k <= K; k++){
for (int p = 0; p <= n >> 1; p++){
for (int t = 0; t <= n - j; t++){
dp[i + 1][j + t][k + (t + p & 1)][t + p >> 1] =
(dp[i + 1][j + t][k + (t + p & 1)][t + p >> 1] +
dp[i][j][k][p] * P[i][t] % mod * C[n - j][t] % mod) % mod;
}
}
}
}
}
int ans = 0;
for (int k = 0; k <= K; k++)
for (int p = 0; p <= n >> 1; p++)
if (k + __builtin_popcount(p) <= K)
ans = (ans + dp[m + 1][n][k][p]) % mod;
cout << ans;
}
不中了,不中了,思维也成一坨了,为了$NOIP$,现在开始吃……
题单链接:题单
[JOISC 2014] 巴士走读 / Bus
原题链接1:[P14418 [JOISC 2014] 巴士走读 / Bus - 洛谷]
原题链接2:#2872. 「JOISC 2014 Day1」巴士走读
分析
我有个不是思路的思路……就是说,我为何不反着想?我改为从$N$出发,这样需要额外记录每个节点的到达最大时间,然后……再稍考一下……呃,似乎并非,需要进行一次二分来确定可以走哪些路径,难道就是这样?但是$1e5$好像并非能过……
咦,好像是对的,好吧,我承认我代码能力很弱……我也承认我很喜欢用省略号……
那么实现起来其实就是做个离线,反向建时间图,依次提高到达终点的时间,跑$Dijkstra$,大概就是我的理解了。燃尽了/ll
正解
#include <bits/stdc++.h>
using namespace std;
const int N = 100005, M = 300005;
struct edge{
int s, t, u;
bool operator < (const edge &tmp) const{
if (s == tmp.s){
if (t == tmp.t){
return u < tmp.u;
}
return t < tmp.t;
}
return s < tmp.s;
}
};
vector<edge> e[N];
int cur_vis[M], cur_mn[M];
int n, m, Q;
struct node{
int x, v;
bool operator < (const node &tmp) const{
return v < tmp.v;
}
};
priority_queue<node> q;
int d[N], sta[N], top;
bool vis[N];
void dijkstra(int tim){
for (int i = 1; i <= top; i++)
vis[sta[i]] = 0;
top = 0;
while (!q.empty()){
q.pop();
}
d[n] = tim;
q.push({n, tim});
edge cur;
while (!q.empty()){
auto tmp = q.top();
int now = tmp.x;
q.pop();
if (now == 1)
return ;
if (vis[now])
continue;
sta[++top] = now;
vis[now] = 1;
while (cur_vis[now] < cur_mn[now]){
cur = e[now][cur_vis[now]];
if (cur.s <= d[now]){
if (cur.t > d[cur.u]){
d[cur.u] = cur.t;
q.push({cur.u, cur.t});
}
++cur_vis[now];
}
else{
break;
}
}
}
}
struct node2{
int tim, id;
bool operator < (const node2 &tmp) const{
return tim < tmp.tim;
}
}qry[N];
int ans[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= m; i++){
int a, b, x, y;
cin >> a >> b >> x >> y;
e[b].push_back({y, x, a});
}
for (int i = 1; i <= n; i++)
sort(e[i].begin(), e[i].end());
for (int i = 1; i <= n; i++)
cur_mn[i] = (int)e[i].size();
cin >> Q;
for (int i = 1; i <= Q; i++){
cin >> qry[i].tim;
qry[i].id = i;
}
sort(qry + 1, qry + Q + 1);
for (int i = 1; i <= n; i++)
d[i] = -1;
for (int i = 1; i <= Q; i++){
dijkstra(qry[i].tim);
ans[qry[i].id] = d[1];
}
for (int i = 1; i <= Q; i++)
cout << ans[i] << '\n';
}
[JOISC 2014] 有趣的家庭菜园 / Growing Vegetables is Fun
原题链接1:[JOISC 2014] 有趣的家庭菜园 / Growing Vegetables is Fun
原题链接2:#2873. 「JOISC 2014 Day1」有趣的家庭菜园
分析
显然,按照之前我们求单峰的做法——求区间最值,然后进行神秘操作是行不通的(因为这题不带修?我也不知道,好像是因为你求出最大值,次大值……之后不同交换代价也不同,硬上DP也是不好做的)。
考虑怎么做,初始情况下,我们给每一株$IOI$草赋一个初始值$pos_i=i$,那么,在最终排好序的序列中,操作数就是$pos$的逆序对。然后,我们开始往最终序列里加值,每次加值,考虑加到西边还是东边,使用树状数组求逆序对,贪心地选择少的那一边(这里产生了对$h_i$降序排序的冲动,是对的),最后就做完了……其实我并不会写带码。
正解
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 300005;
int n;
struct node{
int h, pos;
}a[N];
int c[N];
void add(int x, int v){
for (int i = x; i <= n; i += i & (-i))
c[i] += v;
}
int query(int x){
int ans = 0;
for (int i = x; i; i -= i & (-i))
ans += c[i];
return ans;
}
bool cmp(node x, node y){
return x.h > y.h;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i].h;
a[i].pos = i;
}
sort(a + 1, a + n + 1, cmp);
int ans = 0;
for (int i = 1; i <= n; ){
int j;
for (j = i; j <= n && a[j].h == a[i].h; j++){
int k = query(a[j].pos);
ans += min(k, i - 1 - k);
}
for (j = i; j <= n && a[j].h == a[i].h; j++){
add(a[j].pos, 1);
}
i = j;
}
cout << ans;
}
好吧,蒟蒻要重新理解树状数组求逆序对了……
[JOISC 2014] 历史的研究 / Historical Research
原题链接1:[JOISC 2014] 历史的研究 / Historical Research
原题链接2:#2874. 「JOISC 2014 Day1」历史研究
分析
$cnt$数组前缀和?$1e9$你闹呢?线段树能写吗?unknow……看一眼tag,莫队,下一题!!!
我问问有没有其他做法……死心了,全是在说回滚莫队板子qwq,改天和整体二分一起学。
[JOISC 2014] 拉面比较 / Ramen
原题链接1:[JOISC 2014] 拉面比较 / Ramen
原题链接2:#2875. 「JOISC 2014 Day1」拉面比较
${\color{Yellow}黄题}$哭了/(ㄒoㄒ)/~~发现是交互……
分析
首先考虑$2N$次交互,相当于两次遍历,可以过$N\le 300$。但是原题$N\le400$,没法这样写,考虑优化……那就是经典的分治做法了,对于序列我们先两两分组比较,选取较大值和较小值,需要$\frac{1}{2}N$次,然后,每组再比$\frac{1}{2}N$次,卡到了$\frac{3}{2}N$次,可以通过了。好的,让我们学写一下交互代码,以后可以打CF的了o(** ̄▽ ̄)ブ
正解(LOJ版)
#include <bits/stdc++.h>
#include "ramen.h"
using namespace std;
int Compare(int X, int Y);
void Answer(int X, int Y);
void Ramen(int N){
if (N == 1)
return Answer(0, 0), void();
vector<size_t> la, li;
for (size_t i = 0; i + 1 < N; i += 2){
if (Compare(i, i + 1) > 0){
la.push_back(i);
li.push_back(i + 1);
}
else{
la.push_back(i + 1);
li.push_back(i);
}
}
if (N % 2 == 1){
size_t last = N - 1;
if (Compare(last, la[0]) > 0){
li.push_back(la[0]);
la[0] = last;
}
else if (Compare(last, li[0]) < 0){
la.push_back(li[0]);
li[0] = last;
}
else{
la.push_back(last);
}
}
size_t maxn = la[0];
for (size_t i = 1; i < la.size(); i++){
if (Compare(la[i], maxn) > 0)
maxn = la[i];
}
size_t minn = li[0];
for (size_t i = 1; i < li.size(); i++){
if (Compare(li[i], minn) < 0)
minn = li[i];
}
Answer(minn, maxn);
}
正解(LG版)
#include <bits/stdc++.h>
using namespace std;
int Compare(int X, int Y);
void Answer(int X, int Y);
void Ramen(int N){
if (N == 1)
return Answer(0, 0), void();
vector<size_t> la, li;
for (size_t i = 0; i + 1 < N; i += 2){
if (Compare(i, i + 1) > 0){
la.push_back(i);
li.push_back(i + 1);
}
else{
la.push_back(i + 1);
li.push_back(i);
}
}
if (N % 2 == 1){
size_t last = N - 1;
if (Compare(last, la[0]) > 0){
li.push_back(la[0]);
la[0] = last;
}
else if (Compare(last, li[0]) < 0){
la.push_back(li[0]);
li[0] = last;
}
else{
la.push_back(last);
}
}
size_t maxn = la[0];
for (size_t i = 1; i < la.size(); i++){
if (Compare(la[i], maxn) > 0)
maxn = la[i];
}
size_t minn = li[0];
for (size_t i = 1; i < li.size(); i++){
if (Compare(li[i], minn) < 0)
minn = li[i];
}
Answer(minn, maxn);
}
上课了,讲的AC自动机,晚上估计要写别的题单,先发一篇。
posted on 2025-11-13 17:27 hetao1733837 阅读(24) 评论(0) 收藏 举报
浙公网安备 33010602011771号