The 2024 CCPC Shandong Invitational Contest and Provincial Collegiate Programming Contest
写在前面
比赛地址:https://codeforces.com/gym/105385。
以下按个人向难度排序。
wenqizhi 大爹跑去军训了于是和 dztlb 两个人打打邀请赛爽爽。
I 签到
当且仅当两个字符相邻且相等时有解,检查一下有解的最少步数即可。
code by dztlb:
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int T,n;
char s[N];
signed main(){
cin>>T;
while(T--){
scanf("%s",s);
n=strlen(s);
bool fl=0;
for(int d=0;d<=n;++d){
if(s[(0+d)%n]==s[(d+n-1)%n]){
fl=1;
cout<<d<<'\n';
break;
}
}
if(!fl) puts("-1");
}
return 0;
}
A 二分答案
二分答案一下做完了。
注意在 check 的时候写丑了可能爆 LL。
code by dztlb:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
const int inf=2e18;
int T,n,k,t[N],l[N],w[N];
bool check(int m){
int cnt=0;
for(int i=1;i<=n;++i){
int o=t[i]*l[i]+w[i];
cnt+=(m/o)*l[i];
cnt+=min((m%o)/t[i],l[i]);
if(cnt>=k) return 1;
}
return 0;
}
signed main(){
cin>>T;
while(T--){
cin>>n>>k;
for(int i=1;i<=n;++i){
cin>>t[i]>>l[i]>>w[i];
}
int L=0,R=inf,ans=0;
while(L<=R){
int mid=(L+R)>>1;
if(check(mid)){
ans=mid;
R=mid-1;
}else L=mid+1;
}
cout<<ans<<'\n';
}
return 0;
}
F 简单推式子,枚举,排序
把最终式子列出来,相邻项相减一下,发现对于 \(k=i\) 的答案,等价于选择 \(k\) 个后缀和,且钦定 \(\operatorname{suf}_1\) 必选时的最大值。
于是把求得后缀和降序排个序直接取即可。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
//=============================================================
int n;
LL a[kN], suf[kN];
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
suf[n + 1] = 0;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
for (int i = n; i; -- i) suf[i] = suf[i + 1] + a[i];
LL ans = suf[1];
std::cout << ans << " ";
std::sort(suf + 2, suf + n + 1, std::greater<LL>());
for (int i = 2; i <= n; ++ i) {
ans += suf[i];
std::cout << ans << " ";
}
std::cout << "\n";
}
return 0;
}
K 构造
考虑把唯一的四个角不同的矩形放到左下角。
然后为了保证其他矩形四个角至少两个相同,一个很显然的做法是令其他部分关于对角线对称即可,则可令左上角和右下角的位置相同。
然后就很容易了,详见代码。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1005;
int n;
int a[55][55];
signed main(){
cin>>n;
puts("Yes");
a[1][1]=1,a[1][2]=2;
a[2][1]=3,a[2][2]=4;
for(int i=3;i<=50;++i){
a[1][i]=5;
a[2][i]=5;
a[i][1]=5;
a[i][2]=5;
}
int now=6;
for(int i=3;i<=50;i+=2){
a[i][i]=now;
a[i+1][i]=now+1;
a[i][i+1]=now+1;
a[i+1][i+1]=now+2;
for(int j=i+2;j<=50;++j){
a[i][j]=now+3;
a[i+1][j]=now+3;
a[j][i]=now+3;
a[j][i+1]=now+3;
}
now+=4;
}
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
cout<<a[i][j]<<' ';
} cout<<endl;
}
return 0;
}
D 枚举
大眼观察一下这个数据范围感觉很奇怪啊,\(T=500\)?为啥不出到 \(10^5\)?于是猜测复杂度可能是根号的。
先不考虑时间的限制,则显然最优策略是先购买 \(\left\lfloor\frac{m}{p}\right\rfloor\) 袋面粉,然后卖掉,并不断重复这个过程。在此过程中每轮买入卖出面粉的数量是单调不降的,且很容易计算出经过多少轮之后,一轮中能买入更多面粉。
再考虑时间的限制,买入卖出 \(x\) 袋面粉至少花费时间 \(2x\),容易发现上述过程中,\(\left\lfloor\frac{m}{p}\right\rfloor\) 的取值只有至多 \(\sqrt{t}\) 种。
于是不断枚举计算一轮中买入卖出面粉的数量,直至时间不够用即可,特判下剩余时间内还能进行多少轮,以及最后一轮至多买入卖出多少面粉即可。
总时间复杂度 \(O(T\sqrt t)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
LL p, a, b, q, c, d, m, t;
//=============================================================
LL needt(LL x_) {
return a * x_ + b + c * x_ + d;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> p >> a >> b;
std::cin >> q >> c >> d;
std::cin >> m >> t;
while (1) {
LL x = m / p, delta = p * (x + 1) - m;
if (x == 0) break;
LL k = ceil(1.0 * delta / (x * (q - p)));
bool flag = 0;
if (1ll * k * needt(x) > t) {
flag = 1;
k = t / needt(x);
}
t -= 1ll * k * needt(x);
m += 1ll * k * x * (q - p);
if (flag) {
if (t <= b + d) x = 0;
else x = std::min(x, (t - b - d) / (a + c));
m += 1ll * x * (q - p);
break;
}
}
std::cout << m << "\n";
}
return 0;
}
C DP,计数
典题做多了导致的一直在想唐氏线性 DP,dztlb 大神看了一眼秒了。
发现仅需保证所有相交的线段颜色不同,但颜色可以任选,则实际上仅需求得每条线段可以选择的颜色种类数量即可,将颜色种类数量求积即为答案。
考虑将线段排序后从左到右分配颜色。发现对于线段 \([l, r]\) 可以选择的颜色数量,即 \(k\) 减去与左端点小于 \(l\),右端点大于 \(l\) 的与 \([l, r]\) 相交的线段数量。
一种比较好的实现方式是,把线段拆成左右端点然后排序,差分维护满足上述条件的与当前端点相交的线段数量,并仅在左端点处统计贡献即可。
总时间复杂度 \(O(n\log n)\) 级别。
code by dztlb:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
const int mod=998244353;
int n,k,ans=1;
struct node{
int ty,id;
}p[N*2];
bool cmp(node a,node b){
if(a.id==b.id) return a.ty<b.ty;
return a.id<b.id;
}
int T;
signed main(){
cin>>T;
while(T--){
ans=1;
cin>>n>>k;
for(int i=1;i<=n;++i){
cin>>p[i].id;
p[i].ty=1;
cin>>p[i+n].id;
p[i+n].ty=2;
}
sort(p+1,p+1+2*n,cmp);
int now=0;
for(int i=1;i<=2*n;++i){
if(p[i].ty==1){
ans*=(k-now); ans%=mod;
++now;
}else{
--now;
}
}
cout<<ans<<'\n';
}
return 0;
}
J 最小生成树,贪心
和「CSP-SJX2019」网格图 的思想基本一样的一题。
考虑实际把图建出来做 Kruscal 的过程。显然对于所有连接颜色 \(i, j\) 的边,因为它们的权值相同,则它们一定在按边权排序后相邻。考虑在枚举到这些边时,若它们连接的点全部不联通,则它们会被连续地添加到生成树中。否则会选择此时不联通的点之间的边进行添加——其结果都是使得它们连接的点全部联通。
讨论一下具体的连边策略。对于连接颜色 \(i, j\) 的边,且 \(i, j\) 此时不联通:
- 若 \(i = j\),则会连 \(a_i - 1\) 条边;
- 若 \(i\not= j\),且 \(i, j\) 内部均不联通,则会连 \(a_i + a_j - 1\) 条边;
- 若 \(i\not= j\),且 \(i, j\) 中某一方内部不联通,则会连 \(a_x - 1\) 条边,\(x\) 为不联通的一方。
- 若 \(i\not= j\),且 \(i, j\) 内部均联通,则仅会连 1 条边。
连边后的结果是 \(i, j\) 两方内部均联通,且 \(i, j\) 联通。
考虑将所有边排序后,按照上述策略模拟 Kruscal 进行点集和点集间的连边即可。
发现内部是否联通,仅会在连边时影响端点所在的集合的联通性。于是直接打 tag 维护同一颜色点内部是否连通,再用并查集维护不同颜色点的连通性即可。
时间复杂度瓶颈在于边的排序。总时间复杂度 \(O(n^2\log n^2)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e3 + 10;
//=============================================================
int n;
struct Edge {
int u, v, w;
} e[kN * kN];
int fa[kN], yes[kN];
LL ans, sz[kN];
//=============================================================
bool cmp(const Edge& fir_, const Edge& sec_) {
return fir_.w < sec_.w;
}
int find(int x_) {
return x_ == fa[x_] ? x_ : fa[x_] = find(fa[x_]);
}
void merge(int x_, int y_, int w_) {
int fx = find(x_), fy = find(y_);
if (fx == fy && yes[x_] && yes[y_]) return ;
if (yes[x_] && yes[y_]) {
ans += w_;
} else if (yes[x_]) {
ans += 1ll * sz[y_] * w_;
} else if (yes[y_]) {
ans += 1ll * sz[x_] * w_;
} else if (x_ == y_) {
ans += 1ll * (sz[x_] - 1) * w_;
} else {
ans += 1ll * (sz[x_] + sz[y_] - 1) * w_;
}
yes[x_] = yes[y_] = 1;
fa[fx] = fy;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
for (int i = 1; i <= n; ++ i) std::cin >> sz[i], fa[i] = i, yes[i] = 0;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
int w; std::cin >> w;
e[(i - 1) * n + j] = (Edge) {i, j, w};
}
}
std::sort(e + 1, e + n * n + 1, cmp);
ans = 0;
for (int i = 1; i <= n * n; ++ i) {
auto [u, v, w] = e[i];
merge(u, v, w);
}
std::cout << ans << "\n";
}
return 0;
}
E 线段树,均摊复杂度
好玩题!场上和 std 做法基本一致了但是想的是根号分治于是 T 爆了,教训是 \(O(1)\) 合并的信息直接上线段树就行。
考虑根据一个区间是否产生贡献,将它们分为三个阶段:
- 红球数量大于 1,贡献为 0;
- 红球数量等于 1,贡献为 \(\operatorname{id}^2\);
- 红球数量等于 0,贡献为 0;
当且仅当阶段切换才会产生贡献,且阶段一持续时间是很长的。因此若每次修改后都检查并更新所有被影响的区间,则很多检查是无用的,复杂度会退化为 \(O(nm)\) 级别。
则想到应当仅在区间内红球数量小于等于 1 时,才尝试进行更新。于是一个套路的做法是进行分治,将给定区间分成若干可以维护的规则区间,然后将询问区间挂到规则区间上,在规则区间阶段变化时,才更新被影响询问区间的信息。
赛时想的是考虑分块得到规则区间,并分别维护每个区间对应的整块与散块内的单点,然而每个区间至多会被拆成 \(O(\sqrt {n})\) 个整块和 \(2\sqrt{n}\) 个单点,常数太大了卡不过去呃呃。进一步考虑,发现上述过程中仅需维护区间内红球数量即可,相邻区间信息可以 \(O(1)\) 合并,于是考虑使用线段树维护,将询问区间拆成 \(O(\log n)\) 个线段树区间并挂到上面即可。
可以保证每个询问区间至多被更新 \(4\log n\) 次,则总时间复杂度 \(O(n\log n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
//=============================================================
int n, m, cnt[kN];
LL ans;
//=============================================================
namespace seg {
#define mid ((L_+R_)>>1)
#define ls (now_<<1)
#define rs (now_<<1|1)
const int kNode = kN << 2;
int sum[kNode], tag[kNode];
std::vector<int> query[kNode];
void pushup(int now_) {
sum[now_] = sum[ls] + sum[rs];
}
void check(int now_, int L_, int R_) {
int len = R_ - L_ + 1, newtag = 0, delta = 0;
if (tag[now_] == 2) return ;
if (tag[now_] == 1 && sum[now_] == 0) {
newtag = 2, delta = 1;
} else if (tag[now_] == 0 && sum[now_] == 1) {
newtag = 1, delta = len - 1;
}
if (!newtag) return ;
tag[now_] = newtag;
for (auto id: query[now_]) {
if (cnt[id] == 1) ans -= 1ll * id * id;
cnt[id] -= delta;
if (cnt[id] == 1) ans += 1ll * id * id;
}
}
void build(int now_, int L_, int R_) {
query[now_].clear();
sum[now_] = tag[now_] = 0;
if (L_ == R_) {
sum[now_] = 1;
tag[now_] = 1;
return ;
}
build(ls, L_, mid), build(rs, mid + 1, R_);
pushup(now_);
}
void insert(int now_, int L_, int R_, int l_, int r_, int id_) {
if (l_ <= L_ && R_ <= r_) {
query[now_].push_back(id_);
return ;
}
if (l_ <= mid) insert(ls, L_, mid, l_, r_, id_);
if (r_ > mid) insert(rs, mid + 1, R_, l_, r_, id_);
}
void modify(int now_, int L_, int R_, int pos_) {
if (L_ == R_) {
sum[now_] = 0;
check(now_, L_, R_);
return ;
}
if (pos_ <= mid) modify(ls, L_, mid, pos_);
else modify(rs, mid + 1, R_, pos_);
pushup(now_), check(now_, L_, R_);
}
#undef mid
#undef ls
#undef rs
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> m;
seg::build(1, 1, n);
ans = 0;
for (int i = 1; i <= m; ++ i) {
int l, r; std::cin >> l >> r;
++ l, ++ r;
seg::insert(1, 1, n, l, r, i);
cnt[i] = r - l + 1;
if (cnt[i] == 1) ans += 1ll * i * i;
}
std::cout << ans << " ";
for (int i = 1; i <= n; ++ i) {
LL a; std::cin >> a;
seg::modify(1, 1, n, (a + ans) % n + 1);
std::cout << ans << " ";
}
std::cout << "\n";
}
return 0;
}
/*
3
5 4
2 4
2 3
3 3
0 2
3 2 4 2 0
2 1
1 1
1 0
2 1
0 1
0 0
1
2 1
0 1
0 0
*/
H 贪心,枚举,二分图
场上一眼网络流爆写费用流居然调出来了但是发现费用流只能做第一问第二问假了于是寄寄。
显然当且仅当两个城堡直接相邻时无解。
然后可以预处理出来所有产生冲突的城堡对,且容易发现至多仅有 \(O(n)\) 对。
发现放置一个障碍物能减少的冲突城堡对仅为 1 或 2 个,一个显然的贪心是尽可能地令障碍物产生 2 的贡献,即放置在能同时减少一对横向的和纵向的冲突城堡对的交叉点上,显然此类交叉点也很容易预处理出来,且至多只有 \(O(n)\) 个。
那么要如何贪心地放置,才能使产生 2 的贡献的障碍物贡献呢?考虑到 1 对冲突城堡需要且仅需要 1 个障碍物,1 个障碍物至多贡献 2 对不同方向上的冲突城堡,想到将不同方向上的冲突城堡对转化为二分图上两边的点,将在交叉点上放置贡献为 2 的障碍物转化成二分图上的边,则在二分图上求得最大匹配即可。
然后考虑放置贡献为 1 的障碍物。此时仅需枚举所有二分图上未被匹配的点对应的冲突城堡对,然后在两者之间任意位置放置即可。
上述预处理过程可以大力 map 套 set +大力枚举实现,详见代码。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 1e3 + 10;
const int kM = 2e5 + 10;
//=============================================================
int housenum, bannum, n, maxnode;
int edgenum, head[kN], v[kM], ne[kM];
struct Blank {
int l, r, id;
};
std::vector<pii> ans;
std::map<pii, pii> point;
std::map<int, std::set<pii> > rhouse, chouse;
std::map<int, std::vector<Blank> > rnode, cnode;
int match[kN];
bool vis[kN];
//=============================================================
void Add(int u_, int v_) {
v[++ edgenum] = v_;
ne[edgenum] = head[u_];
head[u_] = edgenum;
}
bool init() {
std::cin >> housenum;
edgenum = 0;
n = 0, maxnode = 4 * housenum;
for (int i = 1; i <= maxnode; ++ i) head[i] = match[i] = 0;
ans.clear(), point.clear();
rhouse.clear(), chouse.clear(), rnode.clear(), cnode.clear();
for (int i = 1; i <= housenum; ++ i) {
int r, c; std::cin >> r >> c;
rhouse[r].insert(mp(c, 0));
chouse[c].insert(mp(r, 0));
}
std::cin >> bannum;
for (int i = 1; i <= bannum; ++ i) {
int r, c; std::cin >> r >> c;
rhouse[r].insert(mp(c, 1));
chouse[c].insert(mp(r, 1));
}
int flag = 0;
for (auto [r, s]: rhouse) {
int last = -1;
for (auto [c, type]: s) {
if (last != -1 && type == 0) {
flag |= (c == last + 1);
rnode[r].push_back((Blank){last + 1, c - 1, ++ n});
}
last = type ? -1 : c;
}
}
for (auto [c, s]: chouse) {
int last = -1;
for (auto [r, type]: s) {
if (last != -1 && type == 0) {
flag |= (r == last + 1);
cnode[c].push_back((Blank){last + 1, r - 1, ++ n});
}
last = type ? -1 : r;
}
}
if (flag) return true;
for (auto [r, sr]: rnode) {
for (auto [l1, r1, id1]: sr) {
for (auto [c, sc]: cnode) {
if (c < l1 || r1 < c) continue;
for (auto [l2, r2, id2]: sc) {
if (r < l2 || r2 < r) continue;
Add(id1, id2);
point[mp(id1, id2)] = mp(r, c);
}
}
}
}
return false;
}
bool dfs(int u_) {
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (vis[v_]) continue;
vis[v_] = 1;
if (!match[v_] || dfs(match[v_])) {
match[v_] = u_;
return 1;
}
}
return 0;
}
void solve() {
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) vis[j] = 0;
dfs(i);
}
for (int i = 1; i <= n; ++ i) vis[i] = 0;
for (int i = 1; i <= n; ++ i) {
if (match[i]) {
ans.push_back(point[mp(match[i], i)]);
vis[i] = vis[match[i]] = 1;
}
}
for (auto [r, s]: rnode) {
for (auto [l1, r1, id]: s) {
if (vis[id]) continue;
ans.push_back(mp(r, r1));
}
}
for (auto [c, s]: cnode) {
for (auto [l1, r1, id]: s) {
if (vis[id]) continue;
ans.push_back(mp(r1, c));
}
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
if (init()) {
std::cout << -1 << "\n";
continue;
}
solve();
std::cout << ans.size() << "\n";
for (auto [x, y]: ans) std::cout << x << " " << y << "\n";
}
return 0;
}
M 区间 DP,计算几何
牛逼。
写在最后
学到了什么:
- C:不限制具体颜色仅限制颜色不同,则仅考虑合法的可选择颜色种类数量即可。
- E:懒更新。
- H:模型很简单,考虑直接贪心 or 枚举即可,不必要嗯上网络流、、、

浙公网安备 33010602011771号