update
12.19:更新 T4 第二种方法,行数100左右,可读性更高,点击这里
CSP22.3 T4通信系统管理
.
10.9:更新 T5,T4 的方法不是很好,看学长的代码100行左右,准备学习一下,挖坑。看到好多收藏,大家有问题或者更好的方法欢迎在评论区交流呀。
题目
http://118.190.20.162/home.page
T1 未初始化警告
思路
vis[] 数组记录变量是否作为左值出现,对赋值语句右值做判断即可。
代码
bool vis[100010];
void solve() {
int n, k;
cin >> n >> k;
int ans = 0;
int x, y;
vis[0] = 1;
for (int i = 1; i <= k; i++) {
cin >> x >> y;
if (!vis[y]) ans++;
vis[x] = 1;
}
cout << ans;
}
T2 出行计划
思路
方法一:对每个场所允许的核酸时间做区间更新(一定是连续的时间段),之后单点查询,使用树状数组进行维护。
方法二:因为只在最后询问一次,可以用差分思想,O(1) 进行区间更新,最后求差分数组的前缀和即可,时间复杂度更优。
代码
// 树状数组,区间加,单点查询
const int N = 4e5 + 10; // 2e5 + 1e5
ll a[N], s[N];
void update(int x, ll y)
{
while (x <= N)
{
s[x] += y;
x += x & (-x);
}
}
ll get(int x)
{
ll res = 0;
while (x > 0)
{
res += s[x];
x -= x & (-x);
}
return res;
}
void solve() {
int n, m, k;
cin >> n >> m >> k;
int t, c;
for (int i = 1; i <= n; i++) {
cin >> t >> c;
int l = t - c + 1;
l = max(1, l);
int r = t;
update(l, 1);
update(r + 1, -1);
}
for (int i = 1; i <= m; i++) {
cin >> t;
cout << get(t + k) << '\n';
}
}
T3 计算资源调度器
思路
模拟题我们分为几个子任务:
一、只考虑节点亲和性。节点亲和性是指该任务要被分到一个指定的可用区,也就是要维护一个可用区中有哪些节点, vector<int> A[1010] 维护。判断节点是否有亲和性要求,若无进行全局搜索,找当前运行任务数量最少的,这里用 int cnt[1010] 对任务数量进行维护,若有则只在 A[na] 中搜索。(50分)
二、再考虑任务的反亲和性。这里我们需要知道一个节点有哪些应用,这个信息之前并无记录,增加 set<int> B[1010] 维护节点运行哪些应用,若节点有反亲和性要求,则搜索到某一个节点时用 set 的 find 方法看看是否冲突。(30分)
三、最后考虑任务的亲和性要求。这里我们需要回到具体的应用运行在哪些可用区再搜索可用区中的节点判断一、二即可,使用 map<int, vector<int>> mp 通过应用编号快速索引至可用区。(20分)
代码
int area[1010]; // 节点所属可用区
int cnt[1010]; // 节点运行任务数量
vector<int> A[1010]; // 可用区包含哪些计算节点
set<int> B[1010]; // 维护节点运行哪些应用
map<int, vector<int>> mp; // 应用运行在哪些可用区
void solve() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> area[i];
A[area[i]].push_back(i); // 有序
}
int g; cin >> g;
int f, a, na, pa, paa, paar; // 至多2000个计算任务
for (int i = 1; i <= g; i++) {
cin >> f >> a >> na >> pa >> paa >> paar;
// 执行f次
while (f--) { // <=2000
int Min = 2005, num = 0;
if (pa) {
// 同时考虑节点亲和性以及任务反亲和
for (auto j : mp[pa]) { // 保证pa出现在前,j为可用区
// 对所有可用区中节点进行甄别
if (na && j != na) continue;
for (int k : A[j]) {
if (paa && B[k].find(paa) != B[k].end()) continue;
if (Min > cnt[k]) {
Min = cnt[k];
num = k;
}
else if (Min == cnt[k] && k < num) num = k;
}
}
if (!num && !paar) {
for (auto j : mp[pa]) {
if (na && j != na) continue;
for (int k : A[j]) {
if (Min > cnt[k]) {
Min = cnt[k];
num = k;
}
else if (Min == cnt[k] && k < num) num = k;
}
}
}
}
else {
if (na) { // 存在节点亲和性要求
for (auto j = A[na].begin(); j != A[na].end(); j++) {
if (paa && B[*j].find(paa) != B[*j].end()) continue; // 反亲和性要求
if (Min > cnt[*j]) {
Min = cnt[*j];
num = *j;
}
}
if (!num && !paar) { // 若没有合适节点对象且反亲和性为尽量满足
for (auto j = A[na].begin(); j != A[na].end(); j++) {
if (Min > cnt[*j]) {
Min = cnt[*j];
num = *j;
}
}
}
}
else {
for (int j = 1; j <= n; j++) {
if (paa && B[j].find(paa) != B[j].end()) continue;
if (Min > cnt[j]) {
Min = cnt[j];
num = j;
}
}
if (!num && !paar) {
for (int j = 1; j <= n; j++) {
if (Min > cnt[j]) {
Min = cnt[j];
num = j;
}
}
}
}
}
cout << num << ' ';
cnt[num]++;
B[num].insert(a);
mp[a].push_back(area[num]);
}
cout << '\n';
}
}
T4 通信系统管理
第二种方法,100行左右,点击这里
CSP22.3 T4通信系统管理
.
以下记录的是第一种方法:
这题被我硬生生玩成了模拟题,写了快300行,巨大常数最后 2s 飘过~
思路
首先,问题非常明确:有 n 个节点,它们相互进行通信,每天为一些节点分配流量,流量是双向的且有有效期,每天询问一些节点的主要通信对象(即所有同它有流量来往的节点中通信流量最大且编号最小者),还有询问“孤岛”以及“通信对”的数量。
求取节点的主要通信对象本质是
动态单点更新,求区间最大值
的问题,但是节点数目达到 1e5,用普通线段树空间上肯定无法满足,动态开点不知道行不行(不会呀)。在最短路算法 Dijkstra 中有一个优先队列它也是求取一个区间最值,也会动态更新,而且用 vis[] 记录已经访问过的节点,若堆顶已经访问过直接弹出。受这个思路启发,我用优先队列 pq[] log 求取每个节点的主通信对象,edge 中 t 记录流量失效时间,若堆顶流量已经失效直接弹出,最后的堆顶就是对象了!
流量减少阶段:同样也是优先队列 Q,以时间为关键字,像是一个沙漏,每天开始时对过期流量进行清除。
流量增加阶段:按要求增加流量,并设置沙漏。
比较重要的流量变动时,要将当前流量重新加入 pq[] ,因此我们需要维护当前实时的流量 vector<edge> g[N],其中 edge 的 t 保存最近的失效时间,那么有一个问题节点之间可能有几股流量,其中一股流量失效后,如何更新 t ?t 应该是下一个最近失效时间。这里我们需要为节点保存所有未到来的失效时间点,同样是使用最小堆map<pair<int, int>, priority_queue<int, vector<int>, greater<int>> > mp 。
主要数据结构:
priority_queue<edge, vector<edge>, cmp> pq[N];
priority_queue<Edge, vector<Edge>, cmp1 > Q; // 沙漏
vector<edge> g[N]; // 实时流量数据,t保存最近失效期
map<pair<int, int>, priority_queue<int, vector<int>, greater<int>> > mp; // 节点之间流量失效期
“孤岛”:判断节点流量增加后是否有了对象或者失效后是否没了对象。
“通信对”:set<pair<int, int> > Set 维护,流量变动之前删除相关节点的通信对,变动之后判断 O[O[u]] == u ,为真加入新的“通信对”。
代码
template<typename _Ty> void __(_Ty &x) {
bool neg = false;
unsigned c = getchar();
for (; (c ^ 48) > 9; c = getchar()) if (c == '-') neg = true;
for (x = 0; (c ^ 48) < 10; c = getchar()) x = (x << 3) + (x << 1) + (c ^ 48);
if (neg) x = -x;
}
template<typename _Ty> _Ty& read(_Ty &x) { __(x); return x; }
template<typename _Ty, typename ..._Tr> void read(_Ty &x, _Tr&... r) { __(x); read(r...); }
const int N = 1e5 + 5;
int O[N]; // 维护节点的主要通信对象
int cnt0, cnt1;
set<pair<int, int> > Set; // 维护通信对
struct Edge
{
int u, v;
ll w;
int t; // t:失效时间
};
struct edge
{
// edge(int v, int w, int t):v(v), w(w), t(t) {}
int v;
ll w;
int t;
};
struct cmp
{
bool operator() (edge a, edge b) {
if (a.w != b.w) return a.w < b.w; // 优先返回流量最大者
return a.v > b.v; // 流量相等返回编号较小者
}
};
struct cmp1
{
bool operator() (Edge a, Edge b) {
return a.t > b.t;
}
};
priority_queue<edge, vector<edge>, cmp> pq[N];
priority_queue<Edge, vector<Edge>, cmp1 > Q; // 沙漏
vector<edge> g[N]; // 实时流量数据,t保存最近失效期
// set<int> set_[N];
map<pair<int, int>, priority_queue<int, vector<int>, greater<int>> > mp; // 节点之间流量失效期
void solve() {
int n, m;
// cin >> n >> m;
read(n, m);
cnt0 = n;
int k, l;
for (int i = 1; i <= m; i++) {
// 1)处理过期流量(流量减少阶段)
while (!Q.empty()) {
auto top = Q.top();
// cout << "top: " << top.u << ' ' << top.v << ' ' << top.w << ' ' << top.t << '\n';
if (i >= top.t) {
int x_ = min(top.u, O[top.u]), y_ = max(top.u, O[top.u]);
if (Set.find({x_, y_}) != Set.end()) {
Set.erase({x_, y_});
cnt1--;
}
x_ = min(top.v, O[top.v]), y_ = max(top.v, O[top.v]);
if (Set.find({x_, y_}) != Set.end()) {
Set.erase({x_, y_});
cnt1--;
}
for (auto &k : g[top.u]) {
if (k.v == top.v) {
k.w -= top.w; // 可能两个节点之间有几股流量,此时如何更新节点之间流量失效期?
int mu = min(top.u, top.v), mv = max(top.u, top.v);
while (!mp[{mu, mv}].empty()) {
int top_ = mp[{mu, mv}].top();
if (i >= top_) {
mp[{mu, mv}].pop();
}
else break;
}
if (mp[{mu, mv}].empty()) { // 节点之间再无流量
// k.t = INT32_MAX;
for (auto it = g[top.u].begin(); it != g[top.u].end(); it++) {
if (it->v == k.v) {
g[top.u].erase(it);
break;
}
}
}
else {
k.t = mp[{mu, mv}].top();
pq[top.u].push({top.v, k.w, k.t});
}
break;
}
}
for (auto &k : g[top.v]) {
if (k.v == top.u) {
k.w -= top.w;
int mu = min(top.u, top.v), mv = max(top.u, top.v);
if (mp[{mu, mv}].empty()) {
// k.t = INT32_MAX;
for (auto it = g[top.v].begin(); it != g[top.v].end(); it++) {
if (it->v == k.v) {
g[top.v].erase(it);
break;
}
}
}
else {
k.t = mp[{mu, mv}].top();
pq[top.v].push({top.u, k.w, k.t});
}
break;
}
}
while(!pq[top.u].empty()) {
auto top_ = pq[top.u].top();
// cout << "u: " << top_.v << ' ' << top_.w << ' ' << top_.t << '\n';
if (top_.t <= i) pq[top.u].pop();
else {
O[top.u] = top_.v;
break;
}
}
while(!pq[top.v].empty()) {
auto top_ = pq[top.v].top();
// cout << "v: " << top_.v << ' ' << top_.w << ' ' << top_.t << '\n';
if (top_.t <= i) pq[top.v].pop();
else {
O[top.v] = top_.v;
break;
}
}
if (O[top.u] && pq[top.u].empty()) O[top.u] = 0, cnt0++;
if (O[top.v] && pq[top.v].empty()) O[top.v] = 0, cnt0++; // 原先有通信对象 -> 没有对象成为信息孤岛
// cout << "cnt0: " << cnt0 << '\n';
// cout << "Objects: ";
// for (int _i = 1; _i <= n; _i++) {
// cout << O[_i] << ' ';
// }
// cout << '\n';
if (O[O[top.u]] == top.u) {
x_ = min(top.u, O[top.u]), y_ = max(top.u, O[top.u]);
Set.insert({x_, y_});
cnt1++;
}
if (O[O[top.v]] == top.v) {
x_ = min(top.v, O[top.v]), y_ = max(top.v, O[top.v]);
if (Set.find({x_, y_}) == Set.end()) {
Set.insert({x_, y_});
cnt1++;
}
}
Q.pop();
}
else break;
}
// 2)添加k个流量并设置倒计时(流量增加阶段)
// cin >> k;
read(k);
int u, v, x, y;
for (int j = 1; j <= k; j++) {
// cin >> u >> v >> x >> y;
read(u); read(v); read(x); read(y);
// read(u, v, x, y);
int x_ = min(u, O[u]), y_ = max(u, O[u]);
if (Set.find({x_, y_}) != Set.end()) {
Set.erase({x_, y_});
cnt1--;
}
x_ = min(v, O[v]), y_ = max(v, O[v]);
if (Set.find({x_, y_}) != Set.end()) {
Set.erase({x_, y_});
cnt1--;
}
if(!O[u]) cnt0--;
if(!O[v]) cnt0--;
if (u > v) swap(u, v);
bool FLAG = 0;
for (auto &k :g[u]) {
if (k.v == v) {
k.w += x;
k.t = min(k.t, i + y); // 设置当前流量的有效期
pq[u].push({v, k.w, k.t});
FLAG = 1;
break;
}
}
for (auto &k :g[v]) {
if (k.v == u) {
k.w += x;
k.t = min(k.t, i + y);
pq[v].push({u, k.w, k.t});
break;
}
}
if (!FLAG) {
g[u].push_back({v, x, i + y});
g[v].push_back({u, x, i + y});
pq[u].push({v, x, i + y});
pq[v].push({u, x, i + y});
}
// u,v主要通信对象可能变化
while(!pq[u].empty()) {
auto top_ = pq[u].top();
if (top_.t <= i) pq[u].pop();
else {
O[u] = top_.v;
break;
}
}
while(!pq[v].empty()) {
auto top_ = pq[v].top();
if (top_.t <= i) pq[v].pop();
else {
O[v] = top_.v;
break;
}
}
if (O[O[u]] == u) {
x_ = min(u, O[u]), y_ = max(u, O[u]);
Set.insert({x_, y_});
cnt1++;
}
if (O[O[v]] == v) {
x_ = min(v, O[v]), y_ = max(v, O[v]);
if (Set.find({x_, y_}) == Set.end()) {
Set.insert({x_, y_});
cnt1++;
}
}
Q.push({u, v, x, i + y});
mp[{u, v}].push(i + y);
}
// 3)回答l个通信主要对象
// cin >> l;
read(l);
int o;
for (int j = 1; j <= l; j++) {
// cin >> o;
read(o);
cout << O[o] << '\n';
}
bool p, q;
// cin >> p >> q;
read(p, q);
if (p) cout << cnt0 << '\n';
if (q) cout << cnt1 << '\n';
}
}
T5 博弈论与石子合并
思路
很显然,小c应该总是丢石子,小z总是合并石子。问题可以分为一下两种情形:
(1)小z先手,且石子有偶数堆。若小c先手且石子为奇数,他可以左右随意丢1堆,转化为这种情形。此时,小z可以通过合并的方式使得自己合并得到石子堆保持在最中间,不被丢弃,你丢右边我就往左边合并,丢左边我合右边,怎么地!因此,最终剩下的应该是中间连续的一段石子堆。对于小c来说,他有决定是哪段连续和的“权利”(小z的策略是被动的,得看小c行事),因此问题转换为
求最小的长度为
(
n
/
2
+
1
)
(n/2+1)
(
n
/2
+
1
)
连续和
,
滑动窗口
即可,代码部分是从丢弃最大的角度考虑的,算的是除去中间部分的首尾两段连续和最大,有点绕了,代码对应 f1()。
(2)小z先手,且石子有奇数堆。同样若小c先手且石子为偶数,他可以左右随意丢1堆,转化为这种情形。此时变成小c可以想方设法丢弃任意一堆石子了,那么小z能做的只有让小c在面对最后两堆时,扔掉较大的一堆而保留的那堆最大(好像等于没说)。注意到小c只能丢弃
(
n
−
1
)
/
2
(n-1)/2
(
n
−
1
)
/2
堆石子,因此
小z只要能通过合并使得一共有
(
n
−
1
)
/
2
+
1
(n-1)/2+1
(
n
−
1
)
/2
+
1
堆石子满足
≥
x
\geq{x}
≥
x
,那么答案至少为x
.虽然是回合制,小c无法保证每次都丢弃
≥
x
\geq{x}
≥
x
的石子堆,但他丢弃的势必是某一
≥
x
\geq{x}
≥
x
堆的一部分,导致这个堆无法合并形成。因此,我们可以
通过二分答案求取x的最大值
,对应代码 f2()。
代码
int a[100010], n, k;
int f1(int l, int r) { // 小z先手,且石子有偶数堆
int m = (r - l), tot = 0; // m%2=0
vector<int> b(m);
for (int i = l; i < r; i++) { b[i - l] = a[i]; tot += a[i]; }
int disc = 0, tmp = 0;
for (int i = 0; i < m / 2 - 1; i++) { tmp += b[i]; }
disc = max(disc, tmp);
for (int i = 0; i < m / 2 - 1; i++) {
tmp += b[m - i - 1] - b[m / 2 - 2 - i];
disc = max(disc, tmp);
}
return tot - disc;
}
// 是否能合并为(n/2+1)个大小至少为x的堆
bool check(int x, int l, int r) {
int sum = 0, cnt = 0;
for (int i = l; i < r; i++) {
sum += a[i];
if (sum >= x) {
sum = 0; cnt++;
}
}
return cnt > (r - l) / 2;
}
int f2(int l, int r) { // 小z先手,且石子有奇数堆
int ret = 1;
int L = 1, R = 1e9;
while (L <= R) {
int mid = (L + R) / 2;
if (check(mid, l, r)) {
ret = mid;
L = mid + 1;
}
else R = mid - 1;
}
return ret;
}
void solve() {
cin >> n >> k;
for (int i = 0; i < n; i++) { cin >> a[i]; }
if ((n % 2 == 0) && k) {
cout << f1(0, n) << '\n';
}
else if ((n % 2 == 1) && (!k)) {
cout << min(f1(1, n), f1(0, n - 1)) << '\n';
}
else if ((n % 2 == 1) && k) {
cout << f2(0, n) << '\n';
}
else if ((n % 2 == 0) && (!k)) {
cout << min(f2(1, n), f2(0, n - 1)) << '\n';
}
}