20230816比赛
T1 矩形
Description
现在我们在一个平面上画了n个矩形。每一个矩形的两边都与坐标轴相平行,且矩形定点的坐标均为整数。现我们定义满足如下性质的图形为一个块:
每一个矩形都是一个块;
如果两个块有一段公共的部分,那么这两个块就会形成一个新的块,否则这两个块就是不同的。
示例:
图1中的矩形形成了两个不同的块。
图2中的矩形形成了一个块。
任务:
请写一个程序:
从文本文件PRO.IN中读入各个矩形的顶点坐标;
找出这些矩形中不同的块的数目;
把结果输出到文本文件PRO.OUT中。
Input
在输入文件PRO.IN的第一行又一个整数n,1 <= n <=7000,表示矩形的个数。接下来的n行描述矩形的顶点,每个矩形用四个数来描述:左下顶点坐标(x,y)与右上顶点坐标(x,y)。每个矩形的坐标都是不超过10000的非负整数。
Output
输出唯一的一个整数——这些矩形所形成的不同的块的数目。
Sample Input
9 0 3 2 6 4 5 5 7 4 2 6 4 2 0 3 2 5 3 6 4 3 2 5 3 1 4 4 7 0 0 1 4 0 0 4 1 Sample Output
2 Data Constraint
数据规模
对于60%的数据,有N<=80
对于100%的数据如题目。
一眼冰茶几(并查集),枚举两个矩形,如果这两个矩形存在公告部分就可以加入并查集。
问题就是怎么判存在公共部分。
我定义了以下函数:
-
check1
用于检查 a 的 x1 或 x2 在 b 的 x1 到 x2 中间+--+ |b | +--+-+| | a| || +--+-+| | | +--+ -
check2
用于检查 a 的 y1 或 y2 在 b 的 y1 到 y2 中间+--+ |a | +--+--+--+ | | | | | +--+b | +--------+ -
check3
用于检查 a 的 端点 与 b 的 端点 重合+--+ |a | +--+--+ | | | b| +--+ -
inside
用于检查 a 在 b 的内部,防止check3误判以下情况+--+--+ | |a | | +--+ | b | | | +-----+
同时讲一个我在比赛时想到,但忘了函数名,现在查到的一个仅在Windows环境下对拍的方法:
链接:intersectRect 函数 (winuser.h) - Win32 apps | Microsoft Learn
intersectRect 函数 (winuser.h)
IntersectRect 函数计算两个源矩形的交集,并将交集矩形的坐标置于目标矩形中。 如果源矩形不相交,则 (将所有坐标设置为零的空矩形,) 放置在目标矩形中。
语法
BOOL IntersectRect( [out] LPRECT lprcDst, [in] const RECT *lprcSrc1, [in] const RECT *lprcSrc2 ); 参数
[out] lprcDst 指向 RECT 结构的指针,用于接收 lprcSrc1 和 lprcSrc2 参数指向的矩形的交集。 此参数不能为 NULL。
[in] lprcSrc1 指向包含第一个源矩形的 RECT 结构的指针。
[in] lprcSrc2 指向包含第二个源矩形的 RECT 结构的指针。
返回值
如果矩形相交,则返回值为非零。
如果矩形不相交,则返回值为零。
注解
由于应用程序可以将矩形用于不同目的,因此矩形函数不使用显式度量单位。 相反,所有矩形坐标和维度都以有符号的逻辑值提供。 使用矩形的映射模式和函数确定度量单位。
示例
有关示例,请参阅 使用矩形。
注意,如果你调试,记得加上 #include <windows.h>
,同时提交时记得删掉。
#include <cstdio> #define ll long long ll n, ans; ll fa[7010]; bool vis[7010]; struct RECT { ll x1, y1, x2, y2; } a[7010]; ll find(ll x) { if(fa[x] == x) return fa[x]; return fa[x] = find(fa[x]); } bool check1(RECT a, RECT b) { return (a.x1 <= b.x2 && b.x2 <= a.x2) || (a.x1 <= b.x1 && b.x1 <= a.x2); } bool check2(RECT a, RECT b) { return (a.y1 <= b.y2 && b.y2 <= a.y2) || (a.y1 <= b.y1 && b.y1 <= a.y2); } bool inside(RECT a, RECT b) { return (b.x1 <= a.x1 && a.x2 <= b.x2 && b.y1 <= a.y1 && a.x2 <= b.x2); } bool check3(RECT a, RECT b) { return !(a.x1 == b.x2 && a.y1 == b.y2 && (inside(a, b) || !inside(b, a))) && !(a.x1 == b.x2 && b.y1 == a.y2 && (inside(a, b) || !inside(b, a))); } int main() { freopen("pro.in", "r", stdin); freopen("pro.out", "w", stdout); scanf("%lld", &n); for(ll i = 1; i <= n; i++) fa[i] = i; for(ll i = 1; i <= n; i++) { scanf("%lld %lld %lld %lld", &a[i].x1, &a[i].y1, &a[i].x2, &a[i].y2); } for(ll i = 1; i <= n; i++) { for(ll j = i + 1; j <= n; j++) { if((check1(a[i], a[j]) || check1(a[j], a[i])) && (check2(a[i], a[j]) || check2(a[j], a[i])) && (check3(a[i], a[j]) && check3(a[j], a[i]))) { ll x = find(i); ll y = find(j); fa[x] = y; } } } for(ll i = 1; i <= n; i++) { if(!vis[find(i)]) { vis[fa[i]] = 1; ans++; } } printf("%lld", ans); }
T2 平坦的折线(lam)
Description
现在我们在一张纸上有一个笛卡尔坐标系。我们考虑在这张纸上用铅笔从左到右画的折线。我们要求任何两个点之间连接的直线段与x轴的夹角在-45~45之间,一条满足以上条件的折线称之为平坦的折线。假定给出了n个不同的整点(坐标为整数的点),最少用几条平坦的折线可以覆盖所有的点?
例子:
图中有6个整点:(1,6), (10,8), (1,5), (2,20), (4,4), (6,2),要覆盖它们至少要3条平坦的折线。任务:
写一个程序:
从文件lam.in中读入点的个数以及它们的坐标。
计算最少需要的折线个数。
将结果写入文件lam.out。Input
在输入文件lam.in的第一行有一个正整数n,不超过30000,代表点的个数。接下来的n行表示这些点的坐标,每行有两个用一个空格隔开的整数x,y,0 <= x <= 30000, 0 <= y <= 30000。第i+1行的数字代表第i个点的坐标。
Output
在输出文件lam.out的第一行应当有且仅有一个整数——表示覆盖所有的点最少需要的折线数。
Sample Input
6 1 6 10 8 1 5 2 20 4 4 6 2 Sample Output
3 Data Constraint
数据规模
对于20%的数据,有N<=15。
对于100%的数据如题目。
比赛时想到可以不用暴力旋转,只需要打一个平衡树(其实当时我想的是我最喜欢的权值线段树)然后找后继就行了,但是处理很麻烦,没打出来。
考虑整个图旋转45°,然后因为题目的要求,我们的折线现在只能严格递增。用f储存折线结尾的y坐标,然后从右到左跑一遍就行了。对于小于当前所有点的y坐标就新开折线,否则二分查找后继(或者平衡树)。
#include <cstdio> #include <set> #include <cmath> #include <climits> #define ll long long using namespace std; ll n; struct POINT { ll x, y; friend bool operator> (const POINT &x, const POINT &y) { if(x.x == y.x) return x.y < y.y; return x.x > y.x; // 注意是从大到小排,具体原因自己想想吧QWQ } } a[30010], g[30010]; void solve(ll l, ll r) { if(l == r) return; ll mid = (l + r) >> 1; solve(l, mid); solve(mid + 1, r); ll pos1 = l, pos2 = mid + 1; for(ll i = l; i <= r; i++) { if(pos2 > r || (pos1 <= mid && a[pos1] > a[pos2])) { g[i] = a[pos1++]; } else { g[i] = a[pos2++]; } } for(ll i = l; i <= r; i++) { a[i] = g[i]; } } ll f[30010], ans; int main() { freopen("lam.in", "r", stdin); freopen("lam.out", "w", stdout); scanf("%lld", &n); for(ll i = 1; i <= n; i++) { ll x, y; scanf("%lld %lld", &x, &y); a[i].x = y - x; a[i].y = y + x; } solve(1, n); f[0] = INT_MAX; for(ll i = 1; i <= n; i++) { if(f[ans] > a[i].y) f[++ans] = a[i].y; // 要求单调递增,所以这里只能开新链了 else { // 选一个最适合的ans来连接 ll l = 1, r = ans, res = 1; while(l <= r) { ll mid = (l + r) >> 1; if(f[mid] <= a[i].y) { res = mid; r = mid - 1; } else { l = mid + 1; } } f[res] = a[i].y; } } printf("%lld", ans); }
T3 Idiot 的间谍网络
Description
作为一名高级特工,Idiot 苦心经营多年,终于在敌国建立起一张共有n 名特工的庞大间谍网络。
当然,出于保密性的要求,间谍网络中的每名特工最多只会有一名直接领导。现在,Idiot 希望整理有关历次特别行动的一些信息。
初始时,间谍网络中的所有特工都没有直接领导。之后,共有m 次下列类型的事件按时间顺序依次发生:
• 事件类型1 x y:特工y 成为特工x 的直接领导。数据保证在此之前特工x 没有直接领导;
• 事件类型2 x:特工x 策划了一起特别行动,然后上报其直接领导审批,之后其直接领导再上报其直接领导的直接领导审批,以此类推,直到某个特工审批后不再有直接领导;
• 事件类型3 x y:询问特工x 是否直接策划或审批过第y 次特别行动。所有特别行动按发生时间的顺序从1 开始依次编号。数据保证在询问之前,第y 次特别行动已经发生过。
作为一名高级特工,Idiot 当然不会亲自办事。于是,Idiot 便安排你来完成这个任务。Input
第一行两个正整数n 和m,分别表示间谍网络中的特工总数,以及事件的总数。
接下来m 行,第i 行给出第i 个事件的信息,格式及含义参见题面。Output
输出共t 行,其中t 表示询问的总数。第i 行输出”Y ES” 或者”NO”,表示第i 次询问的答案。
Sample Input
6 12 2 1 1 4 1 3 4 1 1 3 4 2 3 3 4 1 2 3 3 4 2 3 1 1 3 1 3 3 1 2 1 2 4 Sample Output
NO NO YES YES YES YES Data Constraint
对于30% 的数据,n <= 3 10^3,m <= 5 10^3;
对于60% 的数据,n <=2 * 10^5,m <= 2 * 10^5;
额外20% 的数据,保证在任意时刻,整张间谍网络由若干条互不相交的链构成;
对于100% 的数据,n <= 5 * 10^5,m <= 5 * 10^5;
C + + 选手的程序在评测时使用编译选项-Wl;--stack = 104857600。
考试时看到这道题想的是离线,然后打了个暴力离线加优化。
结果真的是离线。
题目要求我们实现:
- 设置 \(x\) 为 \(y\) 的祖先
- 将 \(x\) 及其祖先打上标记
- 判断在某一时刻 \(x\) 是否有 \(y\) 的标记。
我们稍微转换操作 3. ,可以先把查询储存下来,遍历我们的操作 2. 的行动编号 ,如果刚好有对操作 2. 的查询,那么我们就在进行完操作 2. 后(因为题目保证在询问之前,第 y 次特别行动已经发生过。),查询 \(x\) 是否为行动 \(y\) 的发起者的祖先,这样就可以去掉 某一时刻 这个条件。
那么怎么判断 \(x\) 是 \(y\) 的祖先呢?
说到祖先,我们总可以想到并查集。
那么我们只需要查询两人的祖先即可……吗?
非也,因为可能为这样,在这里,x与y的祖先虽然相同,可是却是y是x的祖先:
那么我们就可以判断两人的深度……吗?
非也,因为可能为这样,在这里,x与y的祖先虽然相同,x与y无关系:
那么应该怎么办呢?这时,时间戳就派上用场了。
当我们在遍历一个节点的子树前,我们先要添加一个 \(st\) 时间戳为 ++ti
。
当我们在遍历完一个节点的子树后,我们先要添加一个 \(ed\) 时间戳为 ++ti
。
那么这样,但凡时间戳在 \([st, ed]\) 内的都是这个节点的子树。
所以我们在判祖先相同的同时还要判时间戳在 \([st, ed]\) 内。
#include <cstdio> #include <algorithm> #define ll long long using namespace std; ll n, m; /* 离线操作 */ struct QUERY { ll op, x, y, id; } query[500010], e[500010], a[500010], d[500010]; ll ecnt, acnt, dcnt; /* 冰茶几 */ ll fa[500010]; ll find(ll x) { if(fa[x] == x) { return x; } return fa[x] = find(fa[x]); } void merge(ll x, ll y) { x = find(x); y = find(y); fa[x] = y; } /* 前向星 */ ll head[500010]; ll nxt[500010]; ll to[500010], cnt; void addEdge(ll u, ll v) { ++cnt; to[cnt] = v; nxt[cnt] = head[u]; head[u] = cnt; } /* 记录时间戳 */ ll st[500010], ed[500010]; bool vis[500010]; ll ti; /* 生成dfn */ void dfs(ll u) { ti++; vis[u] = true; st[u] = ti; for(ll i = head[u]; i; i = nxt[i]) if(!vis[to[i]]){ ll v = to[i]; dfs(v); } ed[u] = ti; } bool ans[500010]; bool cmp(QUERY x, QUERY y) { return x.y < y.y; } int main() { scanf("%lld %lld", &n, &m); for(ll i = 1; i <= n; i++) fa[i] = i; for(ll i = 1; i <= m; i++) { query[i].id = i; scanf("%lld", &query[i].op); switch(query[i].op) { case 1: scanf("%lld %lld", &query[i].x, &query[i].y); addEdge(query[i].y, query[i].x); merge(query[i].x, query[i].y); e[++ecnt] = query[i]; break; case 2: scanf("%lld", &query[i].x); d[++dcnt] = query[i]; break; case 3: scanf("%lld %lld", &query[i].x, &query[i].y); a[++acnt] = query[i]; break; } } for(ll i = 1; i <= n; i++) { ll u = find(i); if(!vis[u]) { dfs(u); } } for(ll i = 1; i <= n; i++) fa[i] = i; // 清空并查集,防止出现后来的上级误以为审批的情况 sort(a + 1, a + 1 + acnt, cmp); // 按照查询的时间顺序进行排序 ll ei = 1, ai = 1; for(ll di = 1; di <= dcnt; di++) { // 枚举特别行动 for(; e[ei].id <= d[di].id; ei++) merge(e[ei].x, e[ei].y); // 把上一次的发起行动,到这一次的发起行动,之间的认爹全部跑一遍 for(; a[ai].y == di; ai++) { // 看有没有查询这次发起行动的 ll u = find(a[ai].x); // 找父亲 ll v = find(d[di].x); if(u == v && // 判断祖先相同,并且 st[a[ai].x] <= st[d[di].x] && st[d[di].x] <= ed[a[ai].x]) { // 这次发起行动在x的子树间 ans[a[ai].id] = 1; } } } for(ll i = 1; i <= m; i++) { if(query[i].op == 3) printf("%s\n", ans[i] ? "YES" : "NO"); } }
T4 Idiot 的乘幂
Description
Input
第一行一个正整数t,表示测试数据组数。
接下来t 行,每行五个正整数a、b、c、d、p,表示一组测试数据。Output
一共t 行,第i 行表示第i 组测试数据的答案。若该组测试数据无解,则输出No Solution!,否则输出一个正整数x(1 <= x < p),表示同余方程组的解。
Sample Input
10 22 1 3 17 24 6 12 5 6 13 16 16 9 1 19 11 1 14 2 23 8 4 17 15 19 10 3 9 1 13 11 1 2 2 23 13 2 14 12 17 11 9 7 11 14 15 10 7 7 17 Sample Output
17 2 5 16 14 3 18 No Solution! 11 12 Data Constraint
暴力,寄。
然后结束后后知后觉,看题目那么多gcd,那肯定得考虑exgcd呀。
把题目中的方程组组合在一起就变成了:
然后我们假定两个数\(x,y\)使得:
那么显然:
所以
那我们就可以根据 \(ax+cy=1\) 跑一遍exgcd,再根据 \(X\equiv b^x\cdot d^y(\mod p)\),就能得出 \(X\) 了。
\(x\) 可能为负数,如果它们为负数,那么它就相当于 \(\frac{1}{b^{-x}}=b^{-1^{-x}}\)
\(y\) 同上。所以如果它们小于0记得还要求个逆元。
#include <cstdio> #define ll long long ll t, a, b, c, d, p, x, y; ll qpow(ll n, ll q) { if(q == 0) return 1; if(q % 2 == 0) { ll res = qpow(n, q / 2); return res * res % p; } return qpow(n, q - 1) * n % p; } ll exgcd(ll a, ll b) { if(b == 0) { x = 1, y = 0; return a; } ll r = exgcd(b, a % b); ll t = x; x = y; y = t - (a/b) * y; return r; } int main() { scanf("%lld", &t); while(t--) { scanf("%lld %lld %lld %lld %lld", &a, &b, &c, &d, &p); exgcd(a, c); ll l, r; ll tmpx = x, tmpy = y; if(tmpx >= 0) { l = qpow(b, tmpx); } else { exgcd(b, p); x = (x % p + p) % p; l = qpow(x, -tmpx); } if(tmpy >= 0) { r = qpow(d, tmpy); } else { exgcd(d, p); x = (x % p + p) % p; r = qpow(x, -tmpy); } if(qpow(l * r % p, a) == b % p && qpow(l * r % p, c) == d % p) { printf("%lld\n", l * r % p); } else { printf("No Solution!\n"); } } }
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现