2012-2013 Asia tokyo regional
南昌区域赛校内PK赛
目录
A 签到 数论相关
B 暴力&方程组
C n阶矩阵快速幂
D 高斯消元解多项式系数
E 搜索(不会)
F 签到 带权并查集
G 计算几何与状态压缩
H (没看)
I 二分答案&dp检验
J (没看)
A
签到题。
根据给出的性质好像就可以暴力从一些必要条件推出结果了。
现在看一下这种运算应该可以被看作是复数a + bi
的乘法运算,从这个角度看的话可能哪怕没有性质的提醒也能自己体会到了。
B
看了下发现不是什么大模拟,而是一个与方程求解有关的问题。
每一行都对应了一个三元一次方程,一共有n行;保证
n
<
=
10
n <= 10
n<=10,
1
<
=
x
i
<
=
20
1<=x_i<=20
1<=xi<=20。
要求给出三个系数,如果可以通过上述方程组得出答案则输出其数值(所以并不需要每个未知量都唯一确定);不然则输出-1.
自己写的时候没有考虑清楚情况,连样例的提示也没有注意到。所以我就写了大概150行的卡拉默法则,还讨论了一阶、二阶、三阶的情况…虽然看起来确实可以解,但是并不是正确做法。
原因在于这并不是一个未知数取 R R R的一般方程组,而是有限制 1 < = x i < = 20 1<=x_i<=20 1<=xi<=20,这也就导致了对于某些方程数量不够的方程组却有可能解出每个未知量的值。那么我这个做法就挂了,因为我的做法无法很漂亮的解决那些因为正整数而夹逼出的未知数答案。
正确的做法在于记录每一个约束条件,然后
O
(
2
0
3
)
O(20^3)
O(203)地枚举答案,对每个可能的
x
i
x_i
xi组合代入所有方程中检验,如果均符合,记录这一组答案。
得出所有可能的答案组后,开始处理
m
m
m行待变换的数据。对于每一组系数,如果所有答案组产生的结果均相同,那么显然不会出现任何矛盾,输出对应答案;如果并不是所有答案组产生的结果都相同,那么就说明之前的约束方程组对应矩阵的秩小于三,并且在之后要求的线性表达式中那些不确定的结果会被用到,也就是说用到了未知的
x
i
x_i
xi,因而输出-1.
AC代码
string str;
struct ruler{
int x, y, z, m;
};
struct node{
int a, b, c;
};
int n, m;
vector<ruler> rule;
vector<node> ans;
int main(){
// Fast;
while(~scanf("%d%d", &n, &m) && n && m){
rule.clear(); ans.clear();
int r, s, c; r = s = c = 0;
cin >> str;
for(auto i: str) switch(i){
case '(': r++; break;
case ')': r--; break;
case '[': s++; break;
case ']': s--; break;
case '{': c++; break;
case '}': c--; break;
default: continue;
}
for(int i = 1; i < n; i++){
int cnt = 0;
cin >> str;
for(auto j: str){
if(j == '.') cnt++;
else break;
}
rule.push_back({r, s, c, cnt});;;
for(auto j: str){
switch(j){
case '(': r++; break;
case ')': r--; break;
case '[': s++; break;
case ']': s--; break;
case '{': c++; break;
case '}': c--; break;
default: continue;
}
}
}
for(int i = 1; i <= 20; i++) for(int j = 1; j <= 20; j++) for(int k = 1; k <= 20; k++){
int ok = 1;
for(auto r: rule){
if(i * r.x + j * r.y + k * r.z != r.m){ok = 0; break;}
}
if(ok) ans.push_back({i, j, k});
}
r = s = c = 0;
printf("0 ");
cin >> str;
for(auto i: str) switch(i){
case '(': r++; break;
case ')': r--; break;
case '[': s++; break;
case ']': s--; break;
case '{': c++; break;
case '}': c--; break;
default: continue;
}
for(int i = 1; i < m; i++){
if(ans.empty()) printf("-1");
else{
int x = ans[0].a, y = ans[0].b, z = ans[0].c;
int res = x * r + y * s + z * c;
int ok = 1;
for(int j = 1, top = (int)ans.size(); j < top; j++){
if(ans[j].a * r + ans[j].b * s + ans[j].c * c != res){ok = 0; break;}
}
if(ok) printf("%d", res);
else printf("-1");
}
if(i != m - 1) printf(" ");
cin >> str;
for(auto j: str) switch(j){
case '(': r++; break;
case ')': r--; break;
case '[': s++; break;
case ']': s--; break;
case '{': c++; break;
case '}': c--; break;
default: continue;
}
}
printf("\n");
}
return 0;
}
C
n阶矩阵的快速幂,写一下熟悉一遍。
(我当时好像因为指针的问题没写出来被赶下去用了白书vector的板子)
const int maxn = 55;
int n, m, a, b, c, t;
int get(int a[maxn][maxn], const int b[maxn][maxn], int r, int c){
ll ans = 0;
for(int i = 0; i < n; i++){
ans += a[r][i] * b[i][c];
ans %= m;
}
return ans % m;
}
struct Mart{
int a[maxn][maxn];
Mart operator *(const Mart &b){
Mart temp;
for(int i = 0; i < n; i++) for(int j = 0; j < n; j++)
temp.a[i][j] = get(a, b.a, i, j);
return temp;
}
};
int ans[maxn];
Mart myMart, E;
int qp(int x, int p){
ll res = p, ans = 1;
while(p){
if(p & 1){
ans *= res;
ans %= m;
}
res *= res; res %= m;
p >>= 1;
}
return ans % m;
}
void change(){
Mart ans(E), res(myMart);
while(t){
if(t & 1) ans = ans * res;
res = res * res;
t >>= 1;
}
myMart = ans;
}
void de(){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++) printf("%d ", myMart.a[i][j]);
printf("\n");
}
puts("END");
}
int main(){
// Fast;
for(int i = 0; i < maxn; i++) E.a[i][i] = 1;
while(~scanf("%d%d%d%d%d%d", &n, &m, &a, &b, &c, &t) && n){
memset(myMart.a, 0, sizeof myMart.a);
for(int i = 0; i < n; i++) scanf("%d", ans + i);
if(n >= 2){
myMart.a[0][0] = b; myMart.a[0][1] = c;
myMart.a[n - 1][n - 2] = a; myMart.a[n - 1][n - 1] = b;
for(int i = 1; i < n - 1; i++){
myMart.a[i][i - 1] = a;
myMart.a[i][i] = b;
myMart.a[i][i + 1] = c;
}
change();
for(int i = 0; i < n; i++){
ll temp = 0;
for(int j = 0; j < n; j++){
temp += myMart.a[i][j] * ans[j];
temp %= m;
}
printf("%lld ", temp % m);
}
printf("\n");
}
else{
printf("%d\n", qp(ans[0], t));
}
}
return 0;
}
D
特殊的高斯消元解方程问题。特殊之处在于不需要寻找第一个拥有非零元的列
,可以知道顺序地从上往下第k列的第k个就是第一个非零元,这也大大简化了算法。
场上由队友写出来了,当时我并不会高斯消元算法,现在其实也不会;但是我有实现算法的能力 C.C
const double eps = 1e-2;
const int maxn = 10;
int d;
double f[maxn];
struct mart{
double a[maxn][maxn];
mart(){memset(a, 0, sizeof a);}
mart(vector<int> vec){
for(int i = 0; i <= d; i++){
a[i][0] = 1;
for(int j = 1; j <= d; j++) a[i][j] = a[i][j - 1] * vec[i];
a[i][d + 1] = f[vec[i]];
}
}
};
vector<int> vec;
mart mar;
double ans[maxn];
int sgn(double n){
return n < -eps? -1: n > eps? 1: 0;
}
//r1行 - k * r2行
void subtract(int r1, double k, int r2){
for(int i = 0; i <= d + 1; i++) mar.a[r1][i] -= k * mar.a[r2][i];
}
void down(int k){
for(int i = k + 1; i <= d; i++) subtract(i, mar.a[i][k] / mar.a[k][k], k);
}
void up(int k){
double temp = 0;
for(int i = k + 1; i <= d; i++) temp += ans[i] * mar.a[k][i];
ans[k] = (mar.a[k][d + 1] - temp) / mar.a[k][k];
}
int check(double x, double y){
double res = ans[0];
double xx = x;
for(int i = 1; i <= d; i++){
res += ans[i] * xx;
xx *= x;
}
return sgn(y - res) == 0;
}
int ok(int exc){
vec.clear();
for(int i = 0; i <= d + 2; i++){
if(exc == i) continue;
vec.push_back(i);
}
mar = mart(vec);
for(int i = 0; i < d; i++) down(i);
for(int i = d; i >= 0; i--) up(i);
if(check(vec[d + 1], f[vec[d + 1]])) return 1;;
return 0;
}
int main(){
// Fast;
while(~scanf("%d", &d) && d){
for(int i = 0; i <= d + 2; i++) scanf("%lf", f + i);
for(int i = 0; i <= d + 2; i++) if(ok(i)) printf("%d\n", i);
}
return 0;
}
这里由于题目输入的函数值小数点后有10位,所以哪怕在正确的系数下也不能保证第d + 2个点完美地在这条函数曲线上,所以eps要放宽一些,不能取为1e-8
,1e-4
能过第二个数据,但是在test2就挂掉了;改成1e-2
完美通过。
F
带权并查集。
签到题。
const int maxn = 1e5 + 10 ;
struct NODE{
int parent;
ll w;
NODE(){w = 0;}
NODE(int x, ll y){
parent = x; w = y;
}
};
int n, m;
NODE node[maxn];
int find(int x){
if(x == node[x].parent) return x;
itn rot = find(node[x].parent);
node[x].w += node[node[x].parent].w;
return node[x].parent = rot;
}
void merge(int x, int y, int w){
int rot1 = find(x), rot2 = find(y);
if(rot1 != rot2){
node[rot2].parent = rot1;
node[rot2].w = node[x].w - w - node[y].w;
}
}
int main(){
// Fast;
while(~scanf("%d%d", &n, &m) && n && m){
for(int i = 1; i <= n; i++) node[i] = {i, 0};
for(int i = 0; i < m; i++){
char op[2]; int x, y, w;
scanf("%s", op);
if(op[0] == '!'){
scanf("%d%d%d", &x, &y, &w);
merge(x, y, w);
}
else{
scanf("%d %d", &x, &y);
int rot1 = find(x), rot2 = find(y);
if(rot1 != rot2) puts("UNKNOWN");
else printf("%lld\n", node[x].w - node[y].w);
}
}
}
return 0;
}
G
在三维空间存在
2
e
3
2e3
2e3个障碍物,
15
15
15个点光源,允许有若干次消除障碍的机会,问可以获得的最大光强是多少?
在贪心的边缘走了一圈发现好像并不可做,然后发现15
可以用状态压缩处理所有状态,就考虑如何在
2
15
=
3
e
4
2^{15} = 3e4
215=3e4种情况下尽可能快的求解需要删除障碍的总数。
如果
O
(
n
m
)
O(nm)
O(nm)地预处理记录每个光源被阻碍的所有障碍物的序号后,再在每一种状态下对每一个取到的光源的阻挡它的障碍物进行一次朴素的合并(比如,桶),每一种状态又要消耗
O
(
n
m
)
O(nm)
O(nm),乘起来的总时间消耗
3
e
4
∗
n
m
=
9
e
8
3e4 * n m = 9e8
3e4∗nm=9e8已经不可接受。所以考虑进行一种更加快速的合并办法。
因为之前在群里见到其他大佬讨论压位?大体思想就是用long long的一些数位表示自己想要的状态,一个long long因为有64位所以可以用一个整数就表示很多的状态。
论水群的好处
这道题好像是可以这么做…=. =…
考虑一个longlong取60位为有效状态,为了表示3000个障碍物的情况,取35个longlong就可以完全地保存3000个障碍物消除与否的情况。这样子在合并的时候只需要对35个longlong进行按位与|
,时间复杂度从3000降到了35,这样子就可以在更快的时间内完成状态合并。
具体说来,状态压缩后要支持三个操作:根据障碍物消除情况来构造longlong数组、根据longlong数组返回原先障碍物的个数以及longlong数组之间的或运算。第一个构造函数遍历所有障碍,取余、取模后得到将要存储状态的横纵坐标;第二个函数时间复杂度 O ( n m ) O(nm) O(nm)遍历每一个longlong的每一位,如果是1就对计数器加1;第三个函数时间复杂度 O ( 35 ) O(35) O(35)(?不严谨的表达),对对应位置之间的longlong直接取或即可。
有一个地方特别需要注意!
在1<<i
的时候,字面值1
默认是int类型!
在1<<i
的时候,字面值1
默认是int类型!
在1<<i
的时候,字面值1
默认是int类型!
为此当我们p == 60(p即为一个longlong中有效状态的个数)
的时候,必须要写成(ll)1 << i
.
到这里可以比较好的处理完状态合并的问题,还有一个问题在于状态的判断上,即在预处理的时候如何检查一根线段被哪些球截断。
到了没做出来过几次的麻烦的立体几何了
最开始写的时候只考虑了球心到光线线段的距离与球的半径的关系,但是因为是光线线段
,仅仅判断距离是不足以说明这根光线一定会被截断,为此我又添加了判断角度,只要两个底角中有一个为钝角(记顶角为以球心为顶点的角,另外两个角为底角),那么这个线段就是一个不会被截断的光线线段。
但是仅仅这样还是不够,因为这样考虑仅仅两个点都在球外面的情况;事实上还有两种比较简单的情况要考虑。一种是两个点都在球内,显然此时这根线段不会被截断;而当一个点在球内一个点在球外的时候这根线段已显然被截断。
定性地考虑完分类问题后考虑一下如何求一点到一条直线的距离,如果连立直线方程实在是太麻烦了。为了有ACM风味一些,可以用三角形三边的长度来表示高度: h a = b ∗ c ∗ s i n A a h_a=\frac{b*c*sinA}{a} ha=ab∗c∗sinA而 s i n A sinA sinA又可以用余弦定理得到,那么高度就可以比较漂亮地解出来了。
最后枚举所有状态,利用之前推导的所有,就可以喜提AC了。
代码
const int maxn = 2e3 + 10;
const int maxm = 16;
const int p = 60;
struct DOT{
double x, y, z;
};
struct BALL{
DOT dot;
double r;
}ball[maxn];
struct LIGHT{
DOT dot;
double t;
}light[maxm];
int n, m, k;
DOT dest;
struct Myhash{
ll a[35];
Myhash(){memset(a, 0, sizeof a);}
Myhash(int select[maxn]){
memset(a, 0, sizeof a);
int row, col;
for(int i = 0; i < n; i++){
if(select[i] == 1){
row = i / p; col = i % p;
a[row] |= (ll)1 << col;
}
}
}
int num(){
int cnt = 0;
for(int i = 0; i < 35; i++){
for(int j = 0; j < p; j++) if((a[i] & ((ll)1 << j)) != 0) cnt++;
if(i * p > n) break;
}
return cnt;
}
Myhash operator |(const Myhash& b){
Myhash temp(*this);
for(int i = 0; i < 35; i++){
temp.a[i] |= b.a[i];
if(i * p > n) break;
}
return temp;
}
};
double getdis(DOT a, DOT b){
double ans = pow(a.x - b.x, 2) + pow(a.y - b.y, 2) + pow(a.z - b.z, 2);
return sqrt(ans);
}
double geth(DOT A, DOT B, DOT C){
double a = getdis(B, C), b = getdis(A, C), c = getdis(A, B);
double sin = (b * b + c * c - a * a) / (2 * b * c);
sin = sqrt(1 - sin * sin);
return b * c * sin / a;
}
int checkangle(DOT A, DOT B, DOT C){
double a = getdis(B, C), b = getdis(A, C), c = getdis(A, B);
if((a * a + c * c - b * b) / (2 * a * c) < 0 || (a * a + b * b - c * c) / (2 * a * b) < 0) return 0;
return 1;
}
//l光源是否被b球挡住?
int in(int l, int b){
double one = getdis(ball[b].dot, dest), two = getdis(ball[b].dot, light[l].dot);
if(one < ball[b].r && two < ball[b].r) return 0;
if((ball[b].r - one) * (ball[b].r - two) < 0) return 1;
if(geth(ball[b].dot, light[l].dot, dest) > ball[b].r) return 0;
return checkangle(ball[b].dot, light[l].dot, dest);
}
int select[maxn];
Myhash myhash[maxm];
//预处理x要扔掉的气球
void prehandle(int x){
for(int i = 0; i < n; i++){
if(in(x, i)) select[i] = 1;
else select[i] = 0;
}
myhash[x] = Myhash(select);
}
double getT(int sts){
double ans = 0;
double temp;
for(int i = 0; i < m; i++) if((sts >> i) & 1){
temp = getdis(dest, light[i].dot);
ans += light[i].t / (temp * temp);
}
return ans;
}
double sove(int sts){
Myhash temp;
for(int i = 0; i < m; i++)
if((sts >> i) & 1) temp = temp | myhash[i];
if(temp.num() > k) return 0;
return getT(sts);
}
int main(){
Fast;
while(~scanf("%d%d%d", &n, &m, &k) && n && m && k){
double x, y, z, r;
for(int i = 0; i < n; i++){
scanf("%lf%lf%lf%lf", &x, &y, &z, &r);
ball[i].dot = {x, y, z};
ball[i].r = r;
}
for(int i = 0; i < m; i++){
scanf("%lf%lf%lf%lf", &x, &y, &z, &r);
light[i].dot = {x, y, z};
light[i].t = r;
}
scanf("%lf%lf%lf", &dest.x, &dest.y, &dest.z);
for(int i = 0; i < m; i++) prehandle(i);
double ans = 0;
for(int i = 0; i < 1 << m; i++) ans = fmax(ans, sove(i));
printf("%.10f\n", ans);
}
return 0;
}
I
在当时打完比赛曾惊奇于二分答案 & dp检验
的办法,然后就学着标答写了一遍…
不过好像并没有什么记忆了,就再写一次练习练习好了
二分答案没有什么新奇之处,重点在于如何快速检验一个假定的最长间距x
是否合适。
记ok[i] = 1
表示第i个(从1开始编号)单词作为结尾,且之前所有文本能在间距不超过x
的时候成功排列的情况。如何做转移呢?如果知道这个单词作为结尾,能够知道其对应可能取的开头区间[l, r]
,那么显然只要在[l - 1, r - 1]
的范围内有一个ok就可以使得ok[i] = 1
了。
怎么快速查询ok数组呢? 前缀和维护就好了。
如何求解left
, right
数组呢? left数组对应间距仅为1、尽可能塞更多个单词在一行的情况,right数组对应间距尽可能大,但是不超过x
的情况。容易发现当从前往后求解ok数组的时候,left
和right
也并不需要重新开始搜索,只需要从上一次位置继续前进即可;因而可以线性地求解这两个数组。
另外,为了求解的方便,可以将左端点的区间形式改为[l, r)
,这样子有利于求解。原因在于求right的时候可以直接用循环while(a[i] - a[r - 1] + (ll)(i - r) * x >= w) r++;
保证出循环的r
就是第一个空隙会大于x
或者r
撞到了i
本身的情况,这都是第一个不能取的情况。
又由题目不对最后一行做要求,所以当i == n
的时候,可以直接将right
置为n
,这样就符合题目的要求。
当获取了左端点的区间后,如果在r > l
的情况下,有l == 1
或者sum[r - 2] - sum[l - 2] > 0
那么这个单词就可以成为一个可能的结尾单词。
最后枚举一下最后一行可能的开头,然后检查其是否到达了1或者之前一个元素是否是ok,满足则输出1不然就输出0。
最后的最后还要注意下long long,如果检验的x开成了int可能在乘的时候爆int…
啰哩啰嗦WA了很多发,写了好多次总算过了。
const int maxn = 5e4 + 10;
int w, n;
ll a[maxn];
int ok[maxn];
int check(itn x){
memset(ok, 0, sizeof ok);
int l = 1, r = 1;
for(int i = 1; i <= n; i++){
while(a[i] - a[l - 1] + i - l > w) l++;
while(a[i] - a[r - 1] + (ll)(i - r) * x >= w) r++; //r所在的位置为第一个总长度小于w,即空隙必须大于x的位置。/ r == i
if(i == n) r = n;
ok[i] = ok[i - 1];
if(r > l){
if(l == 1) ok[i]++;
else if(ok[r - 1] - ok[l - 2] > 0) ok[i]++;
}
}
for(int i = n; i >= 1 && a[n] - a[i - 1] + n - i <= w; i--){
if(i == 1 || ok[i - 1] - ok[i - 2] > 0) return 1;
}
return 0;
}
int half(){
int left = 1, right = w - 2, middle;
while(left < right){
middle = (left + right) >> 1;
if(check(middle)) right = middle;
else left = middle + 1;
}
return left;
}
int main(){
// Fast;
while(~scanf("%d%d", &w, &n) && w && n){
for(int i = 1; i <= n; i++){
scanf("%lld", a + i);
a[i] += a[i - 1];
}
printf("%d\n", half());
}
return 0;
}
参考代码:
[github] 2012-2013 ACM-ICPC, Asia Tokyo Regional Contest
好像网上没有很全的题解,不过这个hub里的代码除了J题都有了,还是比较具有参考价值的
还能学到各种奇怪的码风 比如goto FUCK etc.