B. Bin Packing
题意(1s):
给定两个长方形各自的长和宽,求一个能够将两个长方形都框起来的凸多边形的最小面积。其中两个长方形不能够重叠,但可以随意旋转和翻转。
思路:
首先我们先设定第一个长方形的长和宽分别为\(a\)和\(b\),第二个长方形的长和宽分别为\(c\)和\(d\),那么就会有潜在的四种放法,也就是两个长方形横着并排放,横着竖排放,横竖并排放,横竖竖排放。并且每种方法又分为两种情况。具体的如图。
那么多边形的面积,实际上就是两个黑色长方形所占据的固定的面积,以及分为四种情况的红色三角形的面积。我们只需要去找到这四种情况哪一种情况的红色三角形的面积最小,加上原先的两个黑色长方形的面积就是答案了。
特别注意啊!!!最后面输出一定要保留一位小数,不保留的话会WA。
代码:
#include<bits/stdc++.h>
using namespace std;
void pr(int T, double ans) {
cout << "Case " << T << ": ";
printf("%.1lf\n",ans) ;
}
void solve() {
int T; cin >> T;
for (int i = 1; i <= T; i++) {
double a, b, c, d;
cin >> a >> b >> c >> d;
double ans = a * b + c * d;
//这边是保证a和c作为宽,b和d作为长
if (a > b) swap(a, b);
if (c > d) swap(c, d);
if (a == c || a == d || b == c || b == d) {
pr(i, ans);
continue;
}
double res1;
if (a > c) res1 = d * (a - c);
else res1 = b * (c - a);
double res2;
if (b > d) res2 = c * (b - d);
else res2 = a * (d - b);
double res3;
if (a > d) res3 = c * (a - d);
else res3 = b * (d - a);
double res4;
if (c > b) res4 = a * (c - b);
else res4 = d * (b - c);
//cout << res1 << " " << res2 << " " << res3 << " " << res4 << endl;
double res = min(min(res1, res2), min(res3, res4)) / 2.0;
pr(i, ans + res);
}
}
int main() {
solve();
return 0;
}
H. Hack a Contest
题意(1s):
给定\(T(1 \leq T \leq 100)\)组测试数据,每组测试数据给定两个整数\((N\)和\(M\),分别表示小雁在比赛中对\(M\)道题一共交了\(N\)发。接下来一行给出\(N\)个整数,表示小雁每一发所提交的时间点,再接下来一行给出\(M\)个整数,分别表示小雁每道交了的题交了几发上去。
最后排名的时候优先按照过题数量越多越好排名,过题数相同时比较罚时越少越好排名。罚时计算规则是:WA一发罚时20分钟,加上每一道过的题最后的过题的时间。
然后求,小雁提交题目的顺序是什么的情况下,能够排名最前,输入此时的罚时。(注意,如果一道题已经AC了,那自然就不能再提交了)
思路:
①首先过题数要最多,那么就是说每一道交了的题目都要AC。
②罚时计算按照最后过这道题目的时间计算,也就是越早过题越好,那么就不应该出现,先WA第一题第二题第三题,然后最后面在一次AC完三道题的情况(罚时起飞了),最贪心的情况,就是一道题一道题的WA到AC为止。
③所以我们可以对提交时间进行排序,因为题目没保证他输入的提交时间是严格单调递增的。然后其次是再对每道题提交的次数进行排序。保证交的少的题目先过,罚时起飞的题目最后过。
④最后计算的时候,就是先算WA的罚时,也就是\((N - M) \times 20\),然后加上每道题目的过题时间点。而过题时间点,我们可以用前缀和来维护。
⑤代码中我为了防止出题人当老六,在每道题交的次数那边有交零次的情况,所以特判了交零次的情况我都直接删了,不然的话在前缀和那边就会重复计算时间。
代码:
#include<bits/stdc++.h>
using namespace std;
void solve() {
int T; cin >> T;
for (int t = 1; t <= T; t++) {
int n, m; cin >> n >> m;
vector<int> shijian;
for (int i = 1; i <= n; i++) {
int x; cin >> x;
shijian.push_back(x);
}
vector<int> cishu;
for (int i = 1; i <= m; i++) {
int x; cin >> x;
if (x == 0) continue;
cishu.push_back(x);
}
sort(shijian.begin(), shijian.end());
sort(cishu.begin(), cishu.end());
int ans = (n - m) * 20;
m = cishu.size();
int sum = -1;
for (int i = 0; i < m; i++) {
sum += cishu[i];
ans += shijian[sum];
}
cout << "Case " << t << ": " << ans << endl;
}
}
int main() {
solve();
return 0;
}
I. Inventory
题意(10s):
小雁的书店中有一个体积为\(V\)的书架,和\(N\)种书籍。第\(i\)种书籍在书架上占据着\(v_i\)的体积,并保证\(\sum\limits_{i=1}^nv_i = V\),每天第\(i\)本书籍会卖出去\(x_i\)份。书卖出去了,就需要补货嘛,所以对于第\(i\)种书,小雁每天需要重新装填书架\(\frac{x_i}{v_i}\)次。
现在给定\(T(1 \leq T \leq 100)\)组测试数据,每组数据第一行依次给定\(N\)和\(V\)的值,接下来第二行给定\(N\)个数据,表示每本书每天会卖出去第几份。然后求小雁每天最少需要装填书架多少次。
思路:
这道题呢,可以用拉格朗日乘子法(也叫拉格朗日乘数法),具体的推导和证明请移步百度百科。同时还需要用到的前置知识点是偏导数。
拉格朗日乘数法_百度百科 (baidu.com)
我这里就大致讲解一下怎么用,然后针对这道题又要怎么做。
求偏导
对于一个包含着若干自变量的函数,当我们对某一个自变量求偏导的时候,我们就把其他自变量当成参数,然后该怎么求导就怎么求导就好。举例,假设有一个函数
$$F(x,y) = x ^ 3 + x ^ 2 y + x + 5$$
当我们对这个函数求关于\(x\)的偏导的时候,就会有
$$F'_x(x, y) = 3x ^ 2 + 2yx + 1$$
当我们对这个函数求关于\(y\)的偏导的时候,就会有
$$F'_y(x, y) = x^2$$
拉格朗日乘数法
假设我们已知一个函数\(G(x, y) = 0\)(我们称这个函数为约数条件),然后要求另一个函数\(F(x, y)\)(我们称这个函数为目标函数)的极值,拉格朗日乘子法的意思就是说,当这个点\((x_0, y_0)\)满足,对\(F(x, y)\)求关于\(x\)的偏导的值比上对\(G(x, y)\)求关于\(x\)偏导的值 等于 对\(F(x, y)\)求关于\(y\)的偏导的值比上对\(G(x, y)\)求关于\(y\)偏导的值 的时候。我们把这个点\((x, y)\)带回去函数\(F(x, y)\)就能够求出函数\(F(x, y)\)的极值。
$$\frac{F'_x(x_0, y_0)}{G'_x(x_0, y_0)} = \frac{F'_y(x_0, y_0)}{G'_y(x_0, y_0)}$$
该题目具体推导过程
有了这两个前置知识,我们现在就可以针对这道题,列出以下两个式子。
①:所有书本在书架上占据的体积之和为\(V\),这个也就是约束条件。有:
$$G(v_1, v_2, v_3,\dots, v_n) = v_1 + v_2 + v_3 + \dots + v_n - V = 0$$
②:对于小雁一共要装填的次数,也就是目标函数,有:
$$F(v_1, v_2, v_3,\dots, v_n) = \frac{x_1}{v_1} + \frac{x_2}{v_2} + \frac{x_3}{v_3} + \dots + \frac{x_n}{v_n}$$
因此套用拉格朗日乘子法我们可以得到,当\(v_1, v_2, v_3 \dots v_n\)满足如下条件时,有装填次数的极小值。
$$\dfrac{1}{-\frac{x_1}{v_1^2}} = \dfrac{1}{-\frac{x_2}{v_2^2}} = \dfrac{1}{-\frac{x_3}{v_3^2}} = \dots = \dfrac{1}{-\frac{x_n}{v_n^2}}$$
化简后,即为
$$\frac{v_1^2}{x_1} = \frac{v_2^2}{x_2} = \frac{v_3^2}{x_3} = \dots = \frac{v_n^2}{x_n}$$
等式同时开根,并设\(v_i\)与\(\sqrt{x_i}\)的比值为\(k\),即:
$$\frac{v_1}{\sqrt{x_1}} = \frac{v_2}{\sqrt{x_2}} = \frac{v_3}{\sqrt{x_3}} = \dots = \frac{v_n}{\sqrt{x_n}} = k$$
即:$$v_i = k\sqrt{x_i}$$
我们将这个式子带会约束条件中,可以得到
$$\sum\limits_{i=1}^nv_i = V \qquad \Rightarrow \qquad \sum\limits_{i=1}^nk\sqrt{x_i} = V$$
此时我们就能求出\(k\)的值:
$$k = \frac{V}{\sum\limits_{i = 1}^n\sqrt{x_i}}$$
我们算出\(k\)的值之后, 就可以将\(k\)的表达式带入目标函数中,那么我们可以得到
$$\frac{x_i}{v_i} = \sqrt{x_i}\frac{\sqrt{x_i}}{v_i} = \sqrt{x_i} \cdot \frac{1}{k}$$
也就是说,小雁一共的装填次数就是
$$F = k\cdot\sum\limits_{i = 1}^n \sqrt{x_i}$$
代码:
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int INF = 0x3f3f3f3f;
const int N = 1e6 + 10;
void solve(){
int T; cin >> T;
for (int i = 1; i <= T; i++) {
int n; cin >> n;
double v; cin >> v;
vector<double> x(n);
vector<double> xx(n);
double sum = 0.0;
for (int i = 0; i < n; i++) {
cin >> x[i];
xx[i] = sqrt(x[i]);
sum += xx[i];
}
double k = v / sum;
double ans = sum / k;
cout << "Case " << i << ": " << fixed << setprecision(6) << ans << endl;
}
}
int main(){
cin.tie(0), cout.tie(0)->sync_with_stdio(false);
solve();
return 0;
}
笔者言:
一、在我个人看来这道题很好的结合了数学和编程啊,最后的表达式真的特别漂亮啊。
二、这道题如果直接用 cin,cout的话,会跑八九秒左右,如果超时了的话,可能是代码中常数太大,可以解绑cin,cout或者直接 scanf,printf,三者之间时间差别还是挺大的。从上到下依次是没解绑,解绑,和scanf/printf输入输出的写法的的运行情况。
J. Jump on Axis
题意(1s):
给定\(T(1 \leq T \leq 200)\)组测试数据。小雁一开始站在零点,要到给出的点\(K\)处,每次它走的步数有三种选择,第一种选择一开始是走1步,第二种选择一开始是走2步,第三种选择一开始是走3步,每次选择了其中一种走法后,下一次选择这种走法都会比上一次选择这种走法走多三步。比如说第一次选择了第三种选择,那么他下一次就能够选择走1、2或6步。然后问对于每一个\(K\),小雁最少需要走几步,以及小雁有几种走法。
思路:
我们注意到,如果小雁在走路的过程中想要走出某一种特定的步数,那么他前面一定已经走了前置的步数,比如小雁如果走了9步,那么他前面一定已经走了3步和6步,通过第三种情况走的步数就是18步。
基于此,我么可以去枚举小雁走到终点的时候,一共走了多少种第一种和第二种的情况,然后计算出通过第一种和第二种情况小雁走的步数来算出小雁剩下需要走的步数能否通过第三种选择恰好走到终点。
我们假设小雁选择了\(i\)次第一种选择,\(j\)次第二种选择,\(v\)次第三种选择之后恰好走到终点。那么我们会得到如下三个式子。
$$\begin{cases}I = 1 + 4 + \cdots + 3i - 2 = 0 + 3 + \cdots + 3(i - 1) + i = \frac{3i(i-1)}{2} +i \\ J = 2 + 5 + \cdots + 3j - 1 = 0 + 3 + \cdots + 3(j - 1) + 2j = \frac{3j(j-1)}{2} + 2j \\ V = 3 + 6 + \cdots + 3v = \frac{3v(v + 1)}{2} \\ V= K - I - J \end{cases}$$
代码中用 \(res1\) 来表示 \(K - I - J\)的值,于是就是解方程,看看解不解的出 \(v\) 的值,这里要用求根公式,如果用二分法去求 \(v\) 的值的话,会TLE。
当我们知道了\(i,j,v\)各自的值之后,总共的步数就是 \(i+j+v\),而这种走法所贡献的方案数就是\(\frac{A^{i+j+v}_{i+j+v}}{A^i_i \cdot A^j_j \cdot A^v_v}\),也就是先全排列,然后再去序。同时由于需要对答案取模,所以这里计算的时候要用乘法逆元。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 10;
const ll mod = 1e9 + 7;
vector<ll> inv(N),fact(N),finv(N);
void init() {
inv[1] = 1;
for(int i = 2; i <= N - 5; i++){
inv[i] = mod - inv[mod % i] * (mod / i) % mod;
}
fact[1] = finv[1] = fact[0] = finv[0] = 1;
for(int i = 2; i <= N - 5; i++){
fact[i] = fact[i - 1] * i % mod;
finv[i] = finv[i - 1] * inv[i] % mod;
}
}
void solve(){
init();
int T; cin >> T;
for (int t = 1; t <= T; t++) {
ll k; cin >> k;
ll ans1 = INF; ll ans2 = 0;
for(int i = 0; 3 * i * (i - 1) / 2 + 3 * i <= k; i++){
int res = k - 3 * i * (i - 1) / 2 - 3 * i;
for(int j = 0; 3 * j * (j - 1) / 2 + 2 * j <= res; j++) {
ll res1 = res - 3 * j * (j - 1) / 2 - 2 * j;
ll v = -1;
if(res1 == 0) v = 0;
else {
ll delta = 1 + (ll)24 * res1 ;
delta = sqrt(delta) ;
v = (1 + delta) / 6;
if(3 * v * v - v - 2 * res1 != 0) continue;
}
ll temp = i + j + v;
ans1 = min(ans1, temp);
ans2 += fact[i + j + v] * finv[i] % mod * finv[j] % mod * finv[v] % mod;
ans2 %= mod;
}
}
cout << "Case " << t << ": " << ans1 << " " << ans2 << endl;
}
}
int main(){
solve();
return 0;
}