26CCPC网络赛 DFS|数学|解方程|异或哈希|通信|二分|倍增|期望DP|最短路计数

B

dfs

给一个六面都决定的骰子,现在创造一个新骰子,要求六面的和是n,问两个骰子都随机投,一共36种情况,构造一个方案使得至少赢19种

第一个骰子六面的值很大,朴素的枚举第二个骰子每一面的值复杂度太大。但实际上我们只用考虑是否能赢第一个骰子,所以第二个骰子可以只枚举每一面,能赢第一个骰子的几面。把第一个骰子的值升序排序,假设能赢前iii面,则第二个骰子的值可以直接设为为ai+1a_i+1ai+1,这是赢前iii面花费最小的方式。

最后六面都枚举出来了,如果还有剩余的点数,随便加到某一个骰子上,情况不会变的更差

严格来说第二个骰子6面,每一面都有7种选择(大于第一个骰子前1-6面,或者一面也不大于),总计算量76=117,6497^6=117,64976=117,649,可以接受。

c 复制代码
void solve()
{
    vi a(10);
    rep(i, 1, 6)
    {
        cin >> a[i];
    }
    sort(a.begin() + 1, a.begin() + 7);
    int k;
    cin >> k;

    vi b(10);
    auto &&dfs = [&](auto &&dfs, int j, int sum, int cnt) -> void
    {
        // cout << j << ' ' << cnt << '\n';
        if (sum > k)
        {
            return;
        }
        if (j == 7)
        {
            if (cnt >= 19)
            {
                // cout << cnt << ' ';
                cout << "YES\n";
                b[6] += k - sum;
                rep(i, 1, 6)
                {
                    cout << b[i] << ' ';
                }
                exit(0);
            }
            return;
        }

        rep(i, 1, 6)
        {
            b[j] = a[i] + 1;
            dfs(dfs, j + 1, sum + b[j], cnt + i);
        }
        b[j] = 1;
        dfs(dfs, j + 1, sum + 1, cnt);
    };

    dfs(dfs, 1, 0, 0);
    cout << "NO\n";
}

C

数学 解方程

一个线段以u的速度水平经过,线段两个端点在t时刻的坐标为(xi+ut,y)(x_i+ut,y)(xi+ut,y),现在我们从原点出发,速度v,可以朝任意方向,问不撞到线段(可以与线段端点重合)的前提下,到达y=wy=wy=w的最短时间

虽然我们能走任意路线,但是走曲线时不优的,最优解应该要么是可以沿着y轴直接向上,要么被线段挡住的话,应该是先走一个斜线,和线段的某个端点同时到达一个位置,在向上走直线。

直接走直线的答案是显然的w/vw/vw/v,条件是竖着往上走到y的位置时,线段的两个端点形成的x区间不包含0。对于被挡住的情况,要和线段端点重合,和左右哪个相遇是最优的不一定,可以两个都算了,取max。考虑到两个端点的算法完全一样只是参数不同,写个函数复用

首先和线段端点同时到达,方程为(x+ut)2+y2=(vt)2(x+ut)^2+y^2=(vt)^2(x+ut)2+y2=(vt)2,转换成ttt为主元的一元二次方程形式为(u2−v2)t2+2xut+(x2+y2)=0(u^2-v^2)t^2+2xut+(x^2+y^2)=0(u2−v2)t2+2xut+(x2+y2)=0

接下来提出来A=u2−v2,B=2xu,C=x2+y2A=u^2-v^2,B=2xu,C=x^2+y^2A=u2−v2,B=2xu,C=x2+y2三个参数,用一元二次求根公式即可。有一些分类讨论

  • A为0,B不为0,退化成一元一次方程
  • AB都为0,无解
  • AB都不为0,看δ=B2−4AC\delta=B^2-4ACδ=B2−4AC,小于0也无解
  • 否则有两个解,解的含义是时间,负数无意义,等价于无解,两个解取最小值即为答案
  • 中间所有无解的地方,为了代码简单,都可以直接返回无穷大,最后两个端点分别调用这个函数取最小值。
c 复制代码
ios::sync_with_stdio(false);
cin.tie(nullptr);

int T;
cin >> T;
cout << setprecision(15);
while (T--) {
    double w, x1, x2, y, u, v;
    cin >> w >> x1 >> x2 >> y >> u >> v;

    auto calc = [&](double x) -> double {
        double A = v * v - u * u;
        double B = -2.0 * u * x;
        double C = -(x * x + y * y);
        const double INF = numeric_limits<double>::infinity();
        if (A == 0) {
            if (B == 0) return INF;
            double ans = -C / B;
            return ans > 0 ? ans : INF;
        }
        double delta = B * B - 4.0 * A * C;
        if (delta < 0) return INF;
        double r1 = (-B + sqrt(delta)) / (2.0 * A);
        double r2 = (-B - sqrt(delta)) / (2.0 * A);
        return min(r1 > 0 ? r1 : INF, r2 > 0 ? r2 : INF);
    };

    double ans;
    if ((x1 * v + u * y < 0 && 0 < x2 * v + u * y)) {
        ans = w / v;
    } else {
        ans = min(calc(x1), calc(x2));
        ans += (w - y) / v;
    }
    cout << ans << "\n";
}
return 0;

E

异或哈希

一个长度n的排列,所有两个元素间的空隙,如果三个人中至少有两个同意,左侧的题都比右侧的简单,就可以在这个空隙处做一次划分。三个人,每个人给的都是一个排列,表示他们认为的题目从简单到难的排序,问最后多少个位置被划分

三个人中任意两个都能决定划分。因此枚举所有两个人的组合,有三种,对于每种组合,考虑划分。这可以写个函数复用

函数内部,考虑两个人的排列的前缀1,i1,i1,i,如果两个人的前缀形成的集合是相等的,也就是两个人认为的前iii简单的题是一样的,则这里就存在了一个,至少有两个人认为,左侧题目比右侧简单的划分,在一个答案数组上记录,最后统计这个答案数组上有多少个空隙有记录,就是答案

关键是如何判断两个前缀形成的集合是否相等,这里是个排列,没有重复元素的集合判等,可以用异或哈希,先用随机数接口给每个元素映射到一个随机值,然后用一个集合内元素的随机值的异或和,表示这个集合,如果两个前缀异或和相等就可以认为集合相等。

c 复制代码
const int N = 2e5 + 10;
int a[N][3];
int b[N];
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());

void solve()
{
    int n;
    cin >> n;

    rep(i, 1, n)
    {
        b[i] = rnd();
    }

    rep(i, 0, 2)
    {
        rep(j, 1, n)
        {
            cin >> a[j][i];
        }
    }

    vi cnt(n + 10);
    auto work = [&](int p, int q)
    {
        int h1 = 0, h2 = 0;
        rep(i, 1, n)
        {
            h1 ^= b[a[i][p]];
            h2 ^= b[a[i][q]];
            if (h1 == h2)
            {
                cnt[i]++;
            }
        }
    };

    work(0, 1);
    work(0, 2);
    work(1, 2);

    int ans = 0;
    cnt[n]++;
    rep(i, 1, n)
    {
        if (cnt[i])
        {
            ans++;
        }
    }

    cout << ans << '\n';
}

H

通信

开始给一个长度n排列,第一次输出一个n * n矩阵,要求每一行都是一个排类,且每一行互不相同。第一次输出的矩阵把行打乱,作为第二次运行的输入,需要还原最开始的排列。

做法很多,官方题解是最简单的。由于只打乱了矩阵的行,没打乱列,每列的集合是不变的。那么可以让第i列的集合的众数是pi。

其实n行都直接输出原始排列就能保证第i列的众数是pi,但是还要满足每一行的排列互不相同。做一些微小的改变让每一行互不相同,同时不破坏每一列的众数即可。一种做法是第i行交换p1,pip_1,p_ip1,pi。这样在n行里,n-1行的第i列还是pi,只有第i行的第i列变成了p1,第i列的众数仍然是pi。

这样对于2-n行都可以用众数找到pi。对于第一列,是一个1-n的排列。得不到什么信息。但是p2-pn都确定了,剩余那个没出现的元素就是p1

c 复制代码
void solve(void) {
	string s;
	cin >> s;
	int n;
	cin >> n;

	if (s == "first") {
		vi a(n + 1);
		rep(i, 1, n) {
			cin >> a[i];
		}

		rep(i, 1, n) {
			swap(a[i], a[1]);
			rep(j, 1, n) {
				cout << a[j] << ' ';
			}
			swap(a[i], a[1]);
			cout << '\n';
		}
	} else {
		vvi a(n + 1, vi(n + 1));
		rep(i, 1, n) {
			rep(j, 1, n) {
				cin >> a[i][j];
			}
		}

		vi vis(n + 1);
		vi ans(n + 1);
		rep(j, 2, n) {
			vi cnt(n + 1);
			rep(i, 1, n) {
				cnt[a[i][j]]++;
			}
			pii mx = {0, 0};
			rep(i, 1, n) {
				mx = max(mx, {cnt[i], i});
			}
			ans[j] = mx.second;
			vis[mx.second] = 1;
		}

		rep(i, 1, n) {
			if (!vis[i]) {
				ans[1] = i;
			}
		}

		rep(i, 1, n) {
			cout << ans[i] << ' ';
		}
	}
}

J

二分 贪心 倍增

一个数组a首先可以做一次循环移位。然后划分成最多k段,每一段求和记为序列b,若长度为m,则对b数组跑一个m级流水线,一个元素x在每一级的耗时都是x。问流水线处理完所有元素的最短时间?

手玩或者推式子可以发现,对于一个确定的数组a,流水线的耗时是∑ai+max⁡(bi)\sum a_i+\max(b_i)∑ai+max(bi),也就是a数组元素和,加上b数组最大值(a中元素和最大的一段)

现在就是要最小化a中划分出的最大段的和,考虑二分这个最大段。check内部贪心地检验,能否在每一段都不超过mid的情况下,划分不超过k段。以上是不考虑循环移位,考虑到循环移位,可以转化成把a数组在后面拼接一遍,在这个长度2n的数组上,考虑所有长度n的子数组,只要存在一个子数组能划分不超过k段,check就有解。

如果没有循环移位,贪心划分即可,O(n)O(n)O(n)的。现在有循环移位,要对n个子数组分别检查,如果每个都做一次贪心,check复杂度O(n2)O(n^2)O(n2),不可接受。

划分k段可以看成跳k次,一个长度n的子数组i,i+n−1i,i+n-1i,i+n−1能否划分成不超过k段,等价于跳k次能否到达至少i+ni+ni+n的位置。以低于O(n)O(n)O(n)的复杂度检查跳k次最远能到哪里,这是经典倍增。

首先划窗找到对于每个lll,sum(l,r)≤midsum(l,r)\le midsum(l,r)≤mid的最远rrr在哪,l,rl,rl,r就是lll为左端点能划分的最大区间,看成(l,r+1)(l,r+1)(l,r+1)建边。作为从lll出发跳20=12^0=120=1次最远能到的位置,接着倍增即可。

检查时,对于每个子数组i,i+n−1i,i+n-1i,i+n−1,以iii为起点,倍增跳kkk步,计算能到的最远位置,看是否不小于i+ni+ni+n,倍增跳的复杂度O(log⁡n)O(\log n)O(logn)。

check复杂度O(nlog⁡n)O(n\log n)O(nlogn),整体复杂度O(nlog⁡2n)O(n\log^2n)O(nlog2n)

在实现上,如果一个区间l,r,r=2nl,r,r=2nl,r,r=2n,那么建边会建一个(l,2n+1)(l,2n+1)(l,2n+1),超出1,2n1,2n1,2n范围。这是可以的,但到了2n+12n+12n+1这个点,就只能待在这个点了,所以倍增时初始化f(0,2n+1)=2n+1f(0,2n+1)=2n+1f(0,2n+1)=2n+1,形成一个自环。并且倍增预处理时也要计算2n+12n+12n+1这个位置,保证f(i,2n+1)f(i,2n+1)f(i,2n+1)的值都是正确的

倍增跳k步时,从高到低检查k的二进制位,如果第i位为1则跳2i2^i2i步,从xxx到f(i,x)f(i,x)f(i,x)的位置

c 复制代码
int f[22][N << 1];
void solve(void) {
	int n, m, k;
	cin >> n >> m >> k;
	vi a(2 * n + 1);
	int sum = 0;
	rep(i, 1, n) {
		cin >> a[i];
		sum += a[i];
		a[i + n] = a[i];
	}

	int l = 1, r = sum;
	int res = -1;

	int mx = __lg(n) + 1;
	auto check = [&](int mi)->bool{
		int sum = 0;
		f[0][2 * n + 1] = 2 * n + 1;
		for (int l = 2 * n, r = 2 * n; l >= 1; l--) {
			sum += a[l];
			while (sum > mi) {
				sum -= a[r];
				r--;
			}
			f[0][l] = r + 1;
		}

		rep(i, 1, mx) {
			rep(j, 1, 2 * n + 1) {
				f[i][j] = f[i - 1][f[i - 1][j]];
			}
		}

		rep(i, 1, n) {
			int x = i;
			for (int j = mx; j >= 0; j--) {
				if (k >> j & 1) {
					x = f[j][x];
				}
			}
			if (x >= i + n) {
				return 1;
			}
		}
		return 0;
	};
	while (l <= r) {
		int mi = (l + r) / 2;
		if (check(mi)) {
			res = mi;
			r = mi - 1;
		} else {
			l = mi + 1;
		}
	}

	int ans = sum + (m - 1) * res;
	cout << ans << '\n';
}

K

期望DP 最短路计数

问起点走到终点的路程的期望。对于每个点,如果有网,则会根据出边连接点到达终点的最短路个数加权,然后随机选取,也就是比如(1,2)(1,3)(1,2)(1,3)(1,2)(1,3),2,3到终点的最短路分别有4,5条,则走这两条边的概率分别为4/9,5/9。如果没网,则所有出边等概率随机走。保证图是个DAG

考虑期望dp,如果u能走到v,则通过dpvdp_vdpv转移到dpudp_udpu,也就是需要先算出dpvdp_vdpv。因此我们的dp顺序是把原图边取反,跑一个拓扑排序。原图是个DAG,且联通,取反了肯定还是DAG且联通。

转移时,没网显然是

dpu=(∑dpv+w(u,v))/m dp_u=(\sum dp_v+w(u,v))/m dpu=(∑dpv+w(u,v))/m

其中m是u的出边个数。

有网则是

dpu=(∑cntv×(dpv+w(u,v)))/m dp_u=(\sum cnt_v\times(dp_v+w(u,v)))/m dpu=(∑cntv×(dpv+w(u,v)))/m

其中cntvcnt_vcntv表示v到终点的最短路个数

这个转移按拓扑序转移即可。还需要一个最短路计数,这也是典,我们在dij的同时,除了维护最短路disidis_idisi,还维护一个最短路计数cnticnt_icnti。如果一个松弛比原来更短,则覆盖原来的cntcntcnt,如果松弛和原来的距离相等,则加入原来的cntcntcnt

c 复制代码
const int N = 2e5 + 10;
const int INF = 1e18;
const int M = 998244353;
#define pii pair<int, int>

int qpow(int a, int b)
{
    int res = 1;
    while (b)
    {
        if (b & 1)
        {
            res = res * a % M;
        }
        a = a * a % M;
        b >>= 1;
    }
    return res;
}

int inv(int x)
{
    return qpow(x, M - 2);
}
void solve()
{
    int n, m, t;
    cin >> n >> m >> t;

    string s;
    cin >> s;
    s = ' ' + s;

    vector<vector<pii>> g(n + 10), G(n + 10);
    vi in(n + 10);
    rep(i, 1, m)
    {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        G[v].push_back({u, w});
        in[u]++;
    }

    vi dis(n + 10, INF), cnt(n + 10);

    priority_queue<pii, vector<pii>, greater<pii>> pq;
    pq.emplace(0, t);
    dis[t] = 0;
    cnt[t] = 1;

    while (pq.size())
    {
        auto [d, u] = pq.top();
        pq.pop();

        if (d > dis[u])
        {
            continue;
        }

        for (const auto &[v, w] : G[u])
        {
            int nd = dis[u] + w;
            if (nd < dis[v])
            {
                dis[v] = nd;
                cnt[v] = cnt[u];
                pq.emplace(nd, v);
            }
            else if (nd == dis[v])
            {
                cnt[v] += cnt[u];
                cnt[v] %= M;
            }
        }
    }

    queue<int> q;
    q.push(t);

    vi dp(n + 10);
    while (q.size())
    {
        int u = q.front();
        q.pop();

        for (auto [v, w] : G[u])
        {
            if (--in[v] == 0)
            {
                q.push(v);
            }
        }

        if (s[u] == '1')
        {
            int tot = 0;
            int sum = 0;
            for (auto [v, w] : g[u])
            {
                if (dis[v] + w != dis[u])
                    continue;
                sum += cnt[v] * (dp[v] + w) % M;
                tot += cnt[v];
            }
            tot %= M;
            sum %= M;
            // cout << u << ' ' << sum << ' ' << tot << '\n';
            sum = sum * inv(tot) % M;
            dp[u] = sum;
        }
        else
        {
            int tot = 0;
            int sum = 0;
            for (auto [v, w] : g[u])
            {
                sum += dp[v] + w;
                tot++;
            }
            tot %= M;
            sum %= M;
            sum = sum * inv(tot) % M;
            dp[u] = sum;
        }
    }

    rep(i, 1, n)
    {
        cout << dp[i] << ' ';
    }
}
相关推荐
小贺儿开发2 天前
Unity 局域网遥控幻灯片展示工具 3.0
科技·http·unity·工具·ppt·通信·网页
搞科研的小刘选手5 天前
【中国·海口(线上+线下)】2026年智能感知、通信与海洋应用国际学术会议(ISCMA 2026)
通信·学术会议·会议推荐·智能感知·海洋应用·海口·海南大学
码上有光6 天前
Linux进程通信——共享内存、消息队列和信号量
android·linux·运维·共享内存·通信
Seoyoneh17 天前
呼叫中心系统上云还是本地部署?架构、成本与运维全维度技术对比
人工智能·信息与通信·通信
小贺儿开发18 天前
Unity 家居视频遥控(细节优化)
科技·unity·程序员·udp·视频·工具·通信
Seoyoneh18 天前
智能客服意图识别实战:深度学习与规则引擎融合架构与落地实践
人工智能·信息与通信·通信
物联通信量讯说20 天前
IOTE 2026 释放信号:中国 IoT 出海正在进入“长期运营”阶段
物联网·iot·通信·企业出海
Seoyoneh21 天前
呼叫中心工单系统架构实战:自动建单与闭环流转技术解析
人工智能·信息与通信·通信
Seoyoneh21 天前
智能客服机器人多轮对话与上下文管理:NLU、DST与对话状态机技术实现
人工智能·信息与通信·通信