26寒假营第二场 包围圈结论|贡献法|根号分治

比赛安排(PDF题面存放于本题)

https://ac.nowcoder.com/acm/contest/120562/A

A

思维

三种东西,构造一个序列,让连续三个元素都不同。

每次选三个东西接到序列末尾,每次的三个东西的排列都相同,最后一轮可以不满三个。这样则受限于,如果差距过大,一个用完了,另一个还有超过一个,就不行。

void solve() {
	vi a(3);
	for (int &x : a) {
		cin >> x;
	}
	sort(a.begin(), a.end());
	
	if(a[1]-a[0]>1||a[2]-a[0]>1){
		cout<<"NO\n";
	}
	else{
		cout<<"YES\n";
	}
}

B

思维 博弈

思维太难。。。个人玩淘汰赛,每次选两个人出来比,较小的被淘汰,相等则两个人都被淘汰。问每个人,是否有可能成为活到最后的一个人。

从每种能力值的角度来考虑,一如果最大值出现了奇数次,即使两两同归于尽,也会剩下一个最大的。这个最大的可以赢下剩下的任何人,所以能赢的只有所有最大值。如果出现了偶数次,最大值是肯定赢不了了,最后剩下的最大值一定是偶数个,必然同归于尽,可以先让最大值淘汰一些竞争对手,然后同归于尽,此时会赢的是剩下的最大值,实际上可以控制淘汰哪些人,来决定剩下的最大值是谁,所以除了最大值的任何值都可以赢.

void solve() {
	int n;
	cin >> n;
	map<int, int>mp;
	vi a(n + 1);
	int mx = 0;
	rep(i, 1, n) {
		cin >> a[i];
		mp[a[i]]++;
		mx = max(mx, a[i]);
	}
	if (mp[mx] % 2) {
		rep(i, 1, n) {
			cout << (a[i] == mx);
		}
	} else {
		rep(i, 1, n) {
			cout << (a[i] != mx);
		}
	}
	cout << '\n';
}

C

并查集 包围圈结论

给一些点,形成一些包围圈,问一个点是否处于某个包围圈内?

这里可以用走迷宫的一个经典思想:左手扶墙,如果一个迷宫是可以走出去的,就是没有形成封闭圈,有出口,那么我们左手扶着墙,一直走总可以走出去。应用在这里,是否位于包围圈内,显然就等价于身处一个迷宫中,问能不能走出去,那么可以先往上下左右,走到靠墙的位置,然后检查靠墙走,能不能走出去。

如果一个点的上下左右存在一个方向能走到无穷远,我们用一个特殊点表示无穷远,把这个点和无穷远连上,用并查集维护连通性。然后我们把靠墙的所有点,都和上下左右遇到的第一个点合并,这样如果一个靠墙点,能靠墙走,走出迷宫,走到无穷远,那他就和无穷远在同一个连通块内。

查询时,让查询点找上下左右第一个靠墙点,如果有任何一个方向没有靠墙点,也就是能直接走到无穷远,肯定能走出。否则检查四个靠墙点,是否存在一个,和无穷远在一个连通块内,如果存在也能走出去

查询上下左右方向的第一个靠墙点,可以存所有墙点,然后二分。

int fx[] = {1, -1, 0, 0, 1, 1, -1, -1};
int fy[] = {0, 0, 1, -1, 1, -1, -1, 1};
int f[N];
set<int>sx[N], sy[N];
pii o = {-1, -1};
int find(int x) {
	if (f[x] == x)return x;
	return f[x] = find(f[x]);
}
void merge(int x, int y) {
	x = find(x), y = find(y);
	if (x != y) {
		f[x] = y;
	}
}

pii findl(int x, int y) {
	auto it = sx[x].lower_bound(y);
	if (it == sx[x].begin()) {
		return o;
	}
	it--;
	return {x, (*it) + 1};
}
pii findr(int x, int y) {
	auto it = sx[x].lower_bound(y);
	if (it == sx[x].end()) {
		return o;
	}
	return {x, (*it) - 1};
}
pii findu(int x, int y) {
	auto it = sy[y].lower_bound(x);
	if (it == sy[y].begin()) {
		return o;
	}
	it--;
	return {(*it) + 1, y};
}
pii findd(int x, int y) {
	auto it = sy[y].lower_bound(x);
	if (it == sy[y].end()) {
		return o;
	}
	return {(*it) - 1, y};
}
void solve() {
	int n, q;
	cin >> n >> q;

	map<pii, int>vis, idx;
	vector<pii>p(n);
	rep(i, 0, n - 1) {
		int x, y;
		cin >> x >> y;
		p[i] = {x, y};
		vis[ {x, y}] = 1;
		sx[x].insert(y);
		sy[y].insert(x);
	}

	int cnt = 0;
	vector<pii>a;
	for (auto [x, y] : p) {
		rep(i, 0, 7) {
			int xx = x + fx[i], yy = y + fy[i];
			if (vis[ {xx, yy}])continue;
			idx[ {xx, yy}] = ++cnt;
			a.push_back({xx, yy});
		}
	}

	rep(i, 0, cnt) {
		f[i] = i;
	}
	idx[o] = 0;
	for (auto [x, y] : a) {
		auto [lx, ly] = findl(x, y);
		auto [rx, ry] = findr(x, y);
		auto [ux, uy] = findu(x, y);
		auto [dx, dy] = findd(x, y);

		int now = idx[ {x, y}];
		merge(now, idx[ {lx, ly}]);
		merge(now, idx[ {rx, ry}]);
		merge(now, idx[ {ux, uy}]);
		merge(now, idx[ {dx, dy}]);
	}

	while (q--) {
		int x, y;
		cin >> x >> y;
		auto [lx, ly] = findl(x, y);
		auto [rx, ry] = findr(x, y);
		auto [ux, uy] = findu(x, y);
		auto [dx, dy] = findd(x, y);
		bool flag = 0;
		if (find(idx[ {lx, ly}]) == find(idx[o])) {
			flag = 1;
		}
		if (find(idx[ {rx, ry}]) == find(idx[o])) {
			flag = 1;
		}
		if (find(idx[ {ux, uy}]) == find(idx[o])) {
			flag = 1;
		}
		if (find(idx[ {dx, dy}]) == find(idx[o])) {
			flag = 1;
		}
		if (!flag) {
			cout << "YES\n";
		} else {
			cout << "NO\n";
		}
	}

}

E

构造

构造一个的01矩阵,满足行,列的元素和序列,都是0到n-1的排列。并且相同元素组成的连通块个数为

手玩可以发现,构造一个上三角或者下三角就可以满足行列元素和的要求,难点在于连通块。我们把下三角,每次平移一行或者一列,并不会破坏行列元素和的约束,于是平移试试,手玩发现了一个模式:我们让最大的两行两列呈现一个这样的模式:倒数第二行和倒数第二列都是0,其余是1。。

??01
??01
0000
1101

这样增加了三个1连通块,并且给左上方的子矩阵每一行,每一列都提供了一个,相当于归约到一个问题规模更小的子问题。考虑按这个模式递归下去,每次都能增加三个连通块,直到某一个,剩余需要的连通块不足三个了,考虑在下三角的基础上略微修改,如果差一个连通块,则直接下三角

0000
0001
0011
0111

如果还差两个,则把下三角边上的一个往外挪

0001
0000
0011
0111

如果差三个,就把下三角边上的两个都往外挪

0001
0000
0011
1011

这个构造方式实际上比题目要求还要强,不一定是个连通块,可以构造范围内的任意个连通块。

void solve() {
    int n;
    cin >> n;
    if(n==1){
        cout<<0;
        return;
    }
    int cnt = 1;
    vvi a(n + 1, vi(n + 1));
    for (int i = n; i >= 1; i -= 2) {
        if (cnt + 3 == n) {
            int cur = 0;
            for (int j = 2; j <= i; j++) {
                ++cur;
                for (int k = i; k >= i - cur + 1; k-- ) {
                    a[j][k] = 1;
                }
            }
            a[i][1] = a[1][i] = 1;
            a[i][2] = a[2][i] = 0;
            break;
        } else if (cnt + 2 == n) {
            int cur = 0;
            for (int j = 2; j <= i; j++) {
                ++cur;
                for (int k = i; k >= i - cur + 1; k-- ) {
                    a[j][k] = 1;
                }
            }
            a[i][1]  = 1;
            a[i][2]  = 0;
            break;
        } else if (cnt + 1 == n) {
            int cur = 0;
            for (int j = 2; j <= i; j++) {
                ++cur;
                for (int k = i; k >= i - cur + 1; k-- ) {
                    a[j][k] = 1;
                }
            }
            break;
        } else {
            cnt += 3;
            for (int j = 1; j <= i; j++) {
                a[i][j] = a[j][i] = 1;
            }
            a[i][i - 1] = a[i - 1][i] = 0;
        }
    }
 
    rep(i, 1, n) {
        rep(j, 1, n) {
            cout << a[i][j];
        }
        cout << '\n';
    }
}

题目这个条件其实有更简单的构造方式,这样就可以 alt

F

数论 构造 位运算

对于一个,找到,满足最小。

看起来像是数学结论题,先打表,发现异或最小值就是,并且存在一种解是,根据裴蜀定理,这显然满足,接下来问题是如何找到一个,使得

注意这个奇特的取值范围,意味着我们可以令,这样左移的部分和没有位上的重叠,异或起来就是

void solve() {
    uint64_t n;
    cin >> n;
 
    uint64_t x = n << 32;
    uint64_t y = x + n;
 
    cout << x << ' ' << y << '\n';
    assert(gcd(x, y) == n && (x ^ y) == n);
}

H

贡献法 子数组计数

alt

这个权值的计算方法,就是计算元素种类数的前缀和。但这是子数组计数,情况太多,考虑贡献法,计算每个有多少贡献。从这个角度来讲的话,如果在某个子数组里是第一次出现,假设后面还有个元素,则会产生的贡献,因为在这里第一次出现,使得从这里开始,后面的元素种类数都增加了

那么我们可以枚举元素,假设他是子数组第一次出现,求这样的子数组个数,也就是求子数组左右端点的方案数。左端点,为了让是子数组内第一次出现,我们可以维护每一个值的前一次出现,那么左端点的范围就是,右端点可以随便取,但是不同的右端点,贡献的系数不一样,对于一个,贡献是,因此右端点这边的方案数还需要乘上系数,也就是求,这是等差数列求和,可以

int cal(int x) {
    return (1 + x) * x / 2;
}
void solve() {
    int n;
    cin >> n;
 
    vi a(n + 1);
    rep(i, 1, n) {
        cin >> a[i];
    }
 
    map<int, int>pre;
 
    int ans = 0;
    rep(i, 1, n) {
        int l = i - pre[a[i]];
        pre[a[i]] = i;
        int r = cal(n - i + 1);
 
        ans += l * r;
    }
    cout << ans << '\n';
 
}

如果改成子序列,会难一点,也可以做。仍然考虑贡献法,左边的元素不能选,其他元素任选,维护前缀个数,剩余个元素每个都可以选或不选,也就是。右边元素每个也可以选或不选,也有系数的问题,就是要求。这玩意暴力算是的,不能对每个都暴力算,应该也有组合数学上的线性递推方法

I

诈骗 思维 回文串

一个01矩阵,问从每个点出发,是否都存在一个终点和起点不同,走过的格子组成回文串的路径。

手玩可以发现,如果起点终点都是,途经的都是,不论路径长度奇数偶数,一定是回文的,而这只要求图中至少有两个。对于起点,同理。

所以只需检查是否至少有

void solve() {
    int n, m;
    cin >> n >> m;
    vvi a(n + 1, vi(m + 1));
 
    int c0 = 0, c1 = 0;
    rep(i, 1, n) {
        rep(j, 1, m) {
            char c;
            cin >> c;
            if (c == '1') {
                a[i][j] = 1;
                c1++;
            } else {
                c0++;
            }
        }
    }
 
    rep(i, 1, n) {
        rep(j, 1, m) {
            if (a[i][j]) {
                if (c1 == 1) {
                    cout << "N";
                } else {
                    cout << "Y";
                }
            } else {
                if (c0 == 1) {
                    cout << "N";
                } else {
                    cout << "Y";
                }
            }
        }
        cout << '\n';
    }
}

J

根号分治 多源bfs

每个点的点权是这个点的度数,对每个点,问走到点权更大的点的最短路径长度?

看起来可以多源,对于点权相同的一组点,可以把点权更大的点作为源点,bfs计算到这组点的最短距离。这样每一轮最坏都是的,如果有组,不就炸了?

但是让我们来注意这个奇怪的点权定义,假设点权为的点数,那么有,这是经典根号分治等式,这除了意味着对于每个,都有,还意味着只有个。考虑每个都有一个,那么整个图的度数就是的了,但是度数只能有

所以我们前面那个只会执行轮,就是对的。

void solve() {
    int n, m;
    cin >> n >> m;
 
    vvi g(n + 1);
    vi d(n + 1), a;
    rep(i, 1, m) {
        int u, v;
        cin >> u >> v;
        d[u]++;
        d[v]++;
        g[u].push_back(v);
        g[v].push_back(u);
    }
 
    rep(i, 1, n) {
        a.push_back(d[i]);
    }
    sort(a.begin() + 1, a.end());
    a.erase(unique(a.begin() + 1, a.end()), a.end());
 
    int tot = a.size();
    rep(i, 1, n) {
        d[i] = lower_bound(a.begin(), a.end(), d[i]) - a.begin() + 1;
    }
 
    vvi bin(tot + 1);
    rep(i, 1, n) {
        bin[d[i]].push_back(i);
    }
 
    vi ans(n + 1, -1);
    rep1(i, tot, 1) {
        vi dis(n + 1, inf);
        queue<int>q;
        rep(j, i + 1, tot) {
            for (int x : bin[j]) {
                q.push(x);
                dis[x] = 0;
            }
        }
 
        while (q.size()) {
            int u = q.front();
            q.pop();
            for (int v : g[u]) {
                if (dis[v] > dis[u] + 1) {
                    dis[v] = dis[u] + 1;
                    q.push(v);
                }
            }
        }
 
        for (int x : bin[i]) {
            ans[x] = dis[x];
        }
    }
    rep(i, 1, n) {
        cout << (ans[i] < inf ? ans[i] : -1) << ' ';
    }
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int test = 1;
//  cin >> test;
 
    while (test--) {
        solve();
    }
 
}
全部评论

相关推荐

02-12 20:22
重庆大学 Java
字节暑期刚入职四天,因为是年前,所以很多正职都放假走了,也就没有给我分配mt,然后有一个老哥在我来的时候给我发了一个landing手册,然后还有关于部门业务的白皮书,还有一些业务代码。然后本人是java面的,进来第一次接触go语言&nbsp;前面几天熟悉了一下go的语法和go的框架,可以读但是还不太会写,然后业务白皮书也看的很头疼,包括landing手册里要了解的很多东西说实话我看文档真的快看死了,一个嵌套一个,问题是我还完全不知道咋用这个我了解的东西,还有就是那个项目代码,那个老哥喊我去写写单测,熟悉一下go的语法,但也进行的很困难(这是我第一段实习,之前都是springboot那一套,真不太熟悉这个)想问问大家的建议,就是我从现在开始到在开年回来之前应该做些什么,我目前就一个想法&nbsp;就是复现一个landing手册上的go框架小项目&nbsp;就是相当于帮自己锻炼锻炼怎么写go&nbsp;或者各位大佬有没有更好的锻炼go语法的建议还有就是大家都在说vibe&nbsp;coding,那我应该怎么锻炼自己使用ai的能力,感觉我除了给一些需求然后它给我生成代码,好像就没别的用法了,那些什么工作流、拆解、skill啥的都不知道从哪一个地方开始,包括我现在正在实习,不知道精力该怎么分配,去网上想找找关于agent开发的一些学习流程,说实话,众说纷纭,有的是从python开始打基础然后系统学那些rag&nbsp;prompt&nbsp;langchain&nbsp;mcp等等,有的是说直接找一个github上的ai项目然后反复问ai,我确实有点迷茫,恳求各位大佬能留下你们宝贵的建议,我一定认真反复深刻学习有一说一&nbsp;我觉得字节饭挺好吃的!
双非后端失败第N人:1. go语言我建议你让ai带着你先把基本语法速通了,然后再去用go重新刷你以前刷过的leetcode,这样熟悉起来很快 2. 直接看你们组go项目,里面用***比较复杂,然后把每一个语法现象都喂给ai,一点点看
字节跳动公司福利 1371人发布
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务