2017 China Collegiate Programming Contest Final (CCPC-Final 2017)

目录写在前面EACKJGI写在最后

写在前面

比赛地址:https://codeforces.com/gym/104207。

以下按照个人向难度排序。

妈的怎么感觉有八十万件杂七杂八的事要做。

受不了了,真想直接消失。

这比大学是一秒也不想上了。

E

签到题,看都没看。

code by dztlb:

#include

#include

#include

#include

#include

#include

using namespace std;

#define int long long

inline int read(){

int x=0,f=1; char s;

while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;

while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^'0'),s=getchar();

return x*f;

}

const int N=10005;

int T,n,a[N];

signed main(){

T=read();

for(int j=1;j<=T;++j){

n=read();

int ans=0;

for(int i=1;i<=n;++i) {a[i]=read(),ans+=a[i],ans+=a[i]/10;

if(a[i]%10) ++ans;

}

cout<<"Case #"<

cout<

}

return 0;

}

/*

2

6

13 11 11 11 13 11

6

13 11 11 11 13 11

*/

A

签到。

我没脑子所以打了个表发现是 \(n-1\)。

经典期望,设 \(f_i\) 表示第 \(i\) 个位置对答案的贡献,第 \(i\) 条狗回到第 \(i\) 个笼子的概率为 \(\frac{1}{n}\) 则有 \(f_i = 0\times \frac{1}{n} + 1\times \frac{n-1}{n} = \frac{n - 1}{n}\),则答案即 \(\sum f_i = n\times \frac{n-1}{n} = n-1\)。

//

/*

By:Luckyblock

*/

#include

#define LL long long

//=============================================================

//=============================================================

inline int read() {

int f = 1, w = 0; char ch = getchar();

for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;

for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');

return f * w;

}

//=============================================================

int main() {

//freopen("1.txt", "r", stdin);

int T = read();

for (int i = 1; i <= T; ++ i) {

int n = read();

printf("Case #%d: ", i);

std::cout << std::fixed << std::setprecision(10) << 1.0 * n - 1 << "\n";

}

return 0;

}

C

签到题,看都没看。

code by dztlb:

#include

#include

#include

#include

#include

#include

using namespace std;

#define int long long

inline int read(){

int x=0,f=1; char s;

while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;

while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^'0'),s=getchar();

return x*f;

}

const int N=1e5+5;

int T,x,y,k;

inline bool check(int t){

int sum=t*11*x;

if((11*y-9*x)*(k-t)<=sum) return 1;

return 0;

}

signed main(){

T=read();

for(int j=1;j<=T;++j){

int ans=0;

x=read(),y=read(),k=read();

if(x>y){

cout<<"Case #"<

continue;

}

int l=0,r=k;

while(l<=r){

int mid=(l+r)>>1;

if(check(mid)){

ans=max(ans,k-mid);

r=mid-1;

}else l=mid+1;

}

cout<<"Case #"<

}

return 0;

}

/*

2

5 9 4

10 10 2

*/

K

最智力巅峰的一集。

先打了个表。

形象化地考虑,当轮数增加 1 时,其实只有在最外面一圈的被占领位置的骑士继续向外跳才会产生新的贡献,于是一个显然的想法是对打出来的表做差分,看看每轮新增的被占领位置的数量。

然后发现第 4 轮之后每轮新增的骑士数量是个等差数列,做完了。

std 也是打表呃呃,说是一眼是个二次多项式,牛逼。

//

/*

By:Luckyblock

*/

#include

#define LL long long

const int kN = 20;

//=============================================================

LL f[kN] = {1, 9, 41, 109, 205, 325, 473, 649, 853, 0};

//=============================================================

inline int read() {

int f = 1, w = 0; char ch = getchar();

for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;

for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');

return f * w;

}

void write(__int128 x)

{

if(x<0)

putchar('-'),x=-x;

if(x>9)

write(x/10);

putchar(x%10+'0');

return;

}

//=============================================================

int main() {

//freopen("1.txt", "r", stdin);

int T = read();

for (int i = 1; i <= T; ++ i) {

__int128 n = read();

printf("Case #%d: ", i);

if (n <= 4) printf("%lld\n", f[n]);

else {

n -= 4;

__int128 ans = 205 + n * 92ll + n * (n + 1) * 14ll;

write(ans);

puts("");

}

}

return 0;

}

J

比较一眼的差分约束,但是 vp 的时候想多了一直 wa。

记 \(\operatorname{dis}_i\) 表示从 1 到 \(i\) 所需的时间,首先钦定 \(\operatorname{dis}_i + 1 \le \operatorname{dis}_{i + 1}\),对于给定限制其实只需要考虑两种情况:\(a = b \land c = d\) 和其他即可。

连边策略详见代码。

//

/*

By:Luckyblock

*/

#include

#define LL long long

const int kN = 4e3 + 10;

const int kM = 2e4 + 10;

const LL kInf = 1e18 + 2077;

//=============================================================

int n, m, X;

int edgenum, head[kN], v[kM], ne[kM];

int cnt[kN];

bool vis[kN];

LL w[kM], dis[kN];

//=============================================================

inline int read() {

int f = 1, w = 0; char ch = getchar();

for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;

for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');

return f * w;

}

void Add(int u_, int v_, LL w_) {

v[++ edgenum] = v_;

w[edgenum] = w_;

ne[edgenum] = head[u_];

head[u_] = edgenum;

}

bool SpfaBfs(int start_) {

std::queue q;

for (int i = 0; i <= n; ++ i) {

cnt[i] = vis[i] = 0;

dis[i] = kInf;

}

q.push(start_);

dis[start_] = 0;

vis[start_] = true;

while (!q.empty()) {

int u_ = q.front(); q.pop();

vis[u_] = false;

for (int i = head[u_]; i; i = ne[i]) {

int v_ = v[i], w_ = w[i];

if (dis[u_] + w_ < dis[v_]) {

dis[v_] = dis[u_] + w_;

cnt[v_] = cnt[u_] + 1;

if (cnt[v_] > n) return true;

if (!vis[v_]) q.push(v_), vis[v_] = true;

}

}

}

return false;

}

void Init() {

n = read(), m = read(), X = read();

edgenum = 0;

for (int i = 0; i <= n; ++ i) head[i] = 0;

for (int i = 2; i <= n; ++ i) Add(i, i - 1, -1);

for (int i = 1; i <= m; ++ i) {

int a = read(), b = read(), c = read(), d = read();

if (a == b && c == d) {

Add(d, a, -X);

Add(a, d, X);

} else {

Add(b, c, X-1);

Add(d, a, -X-1);

}

}

for (int i = 1; i <= n; ++ i) Add(0, i, 0);

}

void Solve() {

if (SpfaBfs(0)) {

printf("IMPOSSIBLE\n");

return ;

}

for (int i = 2; i <= n; ++ i) printf("%lld ", dis[i] - dis[i - 1]);

printf("\n");

}

//=============================================================

int main() {

//freopen("1.txt", "r", stdin);

int T = read();

for (int time = 1; time <= T; ++ time) {

printf("Case #%d: ", time);

Init();

Solve();

}

return 0;

}

/*

1

4 2 2

1 2 3 4

2 3 2 3

*/

G

一种显然的 DP 状态是设 \(f_{i, j}\) 表示最远覆盖到位置 \(i\),使用了 \(j\) 个区间时可以覆盖的位置的最大数量。

显然最优的情况仅会使用右端点为 \(i\) 的最长的区间 \([l, i]\) 来转移到 \(f_{i, j}\),朴素的转移是枚举已经覆盖的最靠右的位置 \(k\),则有:

\[f_{i, j} = \max{\left( \max_{0\le k< l}f_{k, j - 1} + (i - l + 1),\ \max_{l\le k< i} f_{k, j - 1} + (i - k)\right)}

\]

然而这样转移是 \(O(n^2k)\) 的,似了。

不过这个状态还是有用的,似的原因在于枚举已经覆盖的最靠右的位置 \(k\) 时有许多无用转移,于是考虑改下状态,记 \(f_{i, j}\) 表示最后覆盖的位置不大于 \(i\),使用了 \(j\) 个区间时可以覆盖的位置的最大数量,即对原来的状态取个前缀最大值。转移时考虑是否需要加一个区间来将 \(i\) 进行覆盖,如果要覆盖则显然会选择覆盖了 \(i\) 的右端点最远的区间 \([l, r]\) 于是转移到 \(f_{r, j + 1}\),否则转移到 \(f_{i + 1, j}\),即有:

\[\begin{aligned}

&f_{i, j} + (r - i + 1)\rightarrow f_{r, j + 1}\\

&f_{i, j} \rightarrow f_{i + 1, j}

\end{aligned}\]

这里的 \(r\) 是可以预处理的,于是复杂度变为 \(O(nk)\) 级别,可过。

这里的第一种转移看似是有缺陷的,当 \(i\) 没有被覆盖的时候 \(r - i + 1\) 其实小于加上这个区间时新增的被覆盖数量,显然最优的第一种转移发生在 \(i\) 恰好为区间 \([l, r]\) 的左端点 \(l\) 时。加上第二种转移后最优的第一种转移会一直被考虑到,所以正确性得到了保证。

//

/*

By:Luckyblock

*/

#include

#define LL long long

const int kN = 2e3 + 10;

//=============================================================

int n, m, k, r[kN];

int f[kN][kN];

//=============================================================

inline int read() {

int f = 1, w = 0; char ch = getchar();

for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;

for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');

return f * w;

}

void Init() {

n = read(), m = read(), k = read();

for (int i = 1; i <= n; ++ i) r[i] = 0;

for (int i = 1; i <= m; ++ i) {

int l_ = read(), r_ = read();

for (int j = l_; j <= r_; ++ j) {

r[j] = std::max(r[j], r_);

}

}

}

void DP() {

for (int i = 0; i <= n; ++ i) {

for (int j = 0; j <= k; ++ j) {

f[i][j] = 0;

}

}

for (int i = 1; i <= n; ++ i) {

for (int j = 1; j <= k; ++ j) {

f[i][j] = std::max(f[i][j], f[i - 1][j]);

if (r[i] > 0) {

f[r[i]][j] = std::max(f[r[i]][j], f[i - 1][j - 1] + r[i] - i + 1);

}

}

}

}

//=============================================================

int main() {

//freopen("1.txt", "r", stdin);

int T = read();

for (int time = 1; time <= T; ++ time) {

Init();

DP();

printf("Case #%d: %d\n", time, f[n][k]);

}

return 0;

}

I

很有意思的题,把连通块种类数转化为各节点连边的种类数之和第一次见,学到许多。

先考虑一棵树的情况,一个由 \(m\) 条颜色相同的边组成的连通块可以覆盖 \(m+1\) 个点,使得这些点相邻的颜色数均 \(+1\),对答案的贡献为 \(1 = (m+1) - m\)。推广到有多个连通块的情况,答案即每个节点连边颜色种类数之和减去总边数 \(n-1\)。

再推广到基环树,答案即每个节点连边颜色种类数之和减去总边数 \(n\)。特别地需要特判环上只有一种颜色的情况,此时答案数还要额外 +1。

思路很简单但是写起来比较绕呃呃。

//

/*

By:Luckyblock

*/

#include

#define LL long long

#define pr std::pair

#define mp std::make_pair

const int kN = 2e5 + 10;

//=============================================================

int n, m, ans, into[kN];

int circle_cnt;

std::vector v[kN];

std::map num[kN];

std::map , int> w;

std::map , bool> incircle;

//=============================================================

inline int read() {

int f = 1, w = 0; char ch = getchar();

for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;

for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');

return f * w;

}

void add(int x_, int c_, bool incircle_) {

num[x_][c_] = num[x_][c_] + 1;

if (num[x_][c_] == 1) ++ ans;

if (incircle_) {

num[0][c_] = num[0][c_] + 1;

if (num[0][c_] == 1) ++ circle_cnt;

}

}

void sub(int x_, int c_, bool incircle_) {

num[x_][c_] = num[x_][c_] - 1;

if (num[x_][c_] == 0) -- ans;

if (incircle_) {

num[0][c_] = num[0][c_] - 1;

if (num[0][c_] == 0) -- circle_cnt;

}

}

void Topsort() {

std::queue q;

for (int i = 1; i <= n; ++ i) {

if (into[i] == 1) q.push(i);

}

while (!q.empty()) {

int u_ = q.front(); q.pop();

for (auto v_: v[u_]) {

incircle[mp(u_, v_)] = incircle[mp(v_, u_)] = 0;

if ((-- into[v_]) == 1) q.push(v_);

}

}

}

void Init() {

n = read(), m = read();

incircle.clear();

w.clear();

for (int i = 0; i <= n; ++ i) {

into[i] = 0;

v[i].clear();

num[i].clear();

}

ans = -n, circle_cnt = 0;

for (int i = 1; i <= n; ++ i) {

int x = read(), y = read(), c = read();

v[x].push_back(y), v[y].push_back(x);

w[mp(x, y)] = w[mp(y, x)] = c;

into[x] ++, into[y] ++;

incircle[mp(x, y)] = incircle[mp(y, x)] = 1;

}

Topsort();

for (int i = 1; i <= n; ++ i) {

for (auto j: v[i]) {

if (i > j) continue;

add(i, w[mp(i, j)], incircle[mp(i, j)]);

add(j, w[mp(i, j)], incircle[mp(i, j)]);

}

}

}

//=============================================================

int main() {

//freopen("1.txt", "r", stdin);

int T = read();

for (int time = 1; time <= T; ++ time) {

printf("Case #%d:\n", time);

Init();

while (m --) {

int x = read(), y = read(), c = read();

sub(x, w[mp(x, y)], incircle[mp(x, y)]);

sub(y, w[mp(x, y)], incircle[mp(x, y)]);

w[mp(x, y)] = w[mp(y, x)] = c;

add(x, w[mp(x, y)], incircle[mp(x, y)]);

add(y, w[mp(x, y)], incircle[mp(x, y)]);

printf("%d\n", ans + (circle_cnt == 1));

}

}

return 0;

}

写在最后

学到了什么:

G:前缀最大值优化状态和转移。

I:把连通块种类数转化为各节点连边的种类数之和

2026-01-18 11:14:28