作者归档:dontpanic

离去

周六这天,不知道哪来的勇气,我在邮件里怼了美国那边的一个Principal同事,晚上七点多才坐下来吃饭。吃到一半就接到爸爸的电话,连夜收拾了一下,坐着早上六点的飞机回家。

这是我这26年的人生历程当中第二次参加葬礼。我坐在一台依维柯的最后一排,车里面都是亲近的亲戚家属。前面是一辆小卡车,拉着一头纸牛和花圈纸钱金砖银元宝。里面坐着我爸,还有我奶奶的遗照。

车里面的亲戚们还在时不时地聊天,聊得我很烦。车开了一会,他们终于不聊了,车里面的广播又变得格外刺耳,广播节目在介绍生活小窍门,比如如何防止铁锅生锈。我时不时地回头从后车窗望去,想看看后面的车队有多长。

奶奶一共兄弟姐妹九个,她是老九,再加上爷爷这边的亲戚,我爸的表亲兄弟姐妹很多。所以我有好几个大姑,好几个老姑,大娘二大娘……逢年过节家里总少不了来串门的亲戚们。

在火葬场的门口,司仪在找人进门帮忙抬老人。我快步往前走了几步,还没走到里面,司仪就说人手已经够了。也许平时好好孩子守规矩惯了,我站在那迟疑了一会,还是想进去再多看奶奶两眼的,直到一个表哥说“你应该进去看看”,我才大步向前走去。

在上小学以前,我经常跟爷爷奶奶住在一起。那时候还住在一间平房,门口有一棵大沙果树。我有一个小小的三轮车,想骑的时候得找奶奶搬出来;我喜欢吃三鲜伊面,奶奶还搬了一箱回来过;没事的时候她经常在床上摆扑克,我就在旁边看,那个时候还学会过几种,不过现在已经全都忘得差不多了。由于爷爷奶奶太偏向,导致我的几个表哥和表姐成立了一个“反孙子集团”,由最大的表哥带头高举“反孙子集团 只在姥姥家”在院子里示威游行。我那时大概只有两三岁的样子,最愿意跟他们玩,还跑过去跟在他们后面一起走,把我爷爷气的不行。

奶奶已经被推到了吊唁厅的中间。也许是最后几个月瘦的,不太像平时的她。她身上盖着红布、放着花,头上还带着以前的那顶小红帽。戴孝带的家属们都站在吊唁厅的一侧,其他亲朋好友们绕着遗体走一圈,鞠个躬以表悼念。吊唁厅里异常寒冷,哀悼乐一响起,让人的心情更加沉重。

后来我上了小学,就跟爸爸妈妈住一起了。爷爷奶奶也搬进了楼房,每到寒假暑假我就住在那,跟奶奶睡一起。除了写寒假作业,我基本上就是跟奶奶看电视剧,什么《还珠格格》、《征服》、《重案六组》、《铁齿铜牙纪晓岚》……都是在那个时候看过的。平时大多是奶奶做饭,派系就是很典型的东北家常菜,土豆炖豆角、酸菜汤之类的。奶奶还会经常自己蒸馒头花卷,有时候碱放多了,馒头就会黄黄的,吃起来带着一股苏打味。

戴孝带的亲戚家属们也走过一圈,鞠过三次躬,奶奶就要被推进去火化了,爸爸和姑姑们几乎到了悲痛的极点。我看着奶奶的遗体,心情非常沉重,但几乎没有眼泪。大家坐在等候室里面,逐渐地开始聊起了天,悲伤的氛围才散去一些。我手里捧着奶奶的遗照,是她之前照的证件照,这是我熟悉的那个奶奶,美丽、开朗和自信。

奶奶一直都有糖尿病和高血压,降糖药、降压药一直都在吃,所幸的是一直没有发生明显的并发症。在爷爷得阿兹海默症之后的一段时间里,奶奶一个人还能照顾得了他。但是毕竟两个老人单独住不太放心,就让他们搬到姑姑家里,让姑姑帮忙照顾。自那以后,也许是觉得自己不再能独立生活,奶奶的心态就越来越差,唉声叹气的时候也越来越多。有一次她想收拾东西搬回去,爸爸和姑姑最终没同意。雪上加霜的是,奶奶开始需要每天打胰岛素来控制血糖,大概这让她更加觉得自己“没什么用”了吧。

捡过骨灰之后,我们就前往墓地了。墓地的工作人员出了点差错,墓还没打扫完,字也还没刻。等过一会,终于可以将骨灰盒下葬,墓碑前也摆好了遗照、贡果和烟酒,角落上放着两头手掌大的石狮子。后面的地上的两挂鞭炮劈里啪啦地响起来,我看着照片,眼泪止不住地留下来。

奶奶的病情恶化的很快。一开始还只是忘事、不爱说话,直到最后连我也认不出了;吞咽功能也受到了影响,从吃饭只能一点一点咽,到最后只能喝米汤。十一我回家的时候,奶奶已经基本不说话了,身体左半边也已经瘫痪无力。真的没想到,短短两个多月的时间,她就从我们的身边离去。

我跟我妈已经达成一致,准备给我爸买台车。我爷爷现在虽然有点糊涂,但生活上还能半自理,也还能走路。趁着现在,多带我爷出门转转,每次回去也能方便一点。

家里电视下面,还摆着爷爷奶奶、三个姑姑和爸爸六口人几年前在公园的照片。现在却已经少了一人。


WebApp for Desktop: 请不要滥用手型指针

这是一篇吐槽。最近想用Electron做点东西,大致浏览了几个UI库,又想起一些用Electron做的App的糟糕体验,实在是想吐槽一番。也不知道大家是不是也有类似的感觉,还是只是我个人吹毛求疵。如果是我的问题,还请打醒我。

首先,这里的WebApp指的是用基于Web的技术制作的客户端程序,比如VSCode、Microsoft Teams、Github Desktop等等。我在使用VSCode和Microsoft Teams时,在用户体验上会跟NativeApp有严重的割裂感。除了渲染性能这种客观问题之外,最主要的问题是,手型指针被滥用了

到处都是手型指针!

举例来说,在VSCode中,把鼠标放在一切能够点击的东西上,几乎都会变成手型,比如文件列表、文件Tabs、各种按钮等等:

然而,在主流的Windows/Gnome/KDE/macOS上,这些都不应该触发手型指针:

为什么在WebApp里面不应该大量使用手型指针?

因为滥用手型指针违背了各种Native UX设计指南——即,这就不是Native App的Feel。例如,在微软的Windows Desktop UxGuide中,明确说明了普通指针和手型指针的适用情况:

Normal Select – Used for most objects.
Link select – Used for text and graphics links because of their weak affordance.

在苹果的Human Interface Guidelines中,同样明确说明了普通指针和手型指针的适用情况:

Arrow – This is the standard pointer that’s used to select and interact with content and interface elements.
Pointing hand – The content beneath the pointer is a URL link to a webpage, document, or other item.

总结一下就是:只有在文本图片链接等情况下,才会推荐使用手型指针。所有一般情况,都应该使用普通的指针。

虽然手型指针为用户提供了额外的提示,表示这个元素可以被鼠标操作,但是在Native App中,很多时候不需要、也不应该依靠手型指针来增强操作提示。在微软的Windows App UxGuide中,有这样一段话:

Well-designed user interface (UI) objects are said to have affordance, which are visual and behavioral properties of an object that suggest how it is used.

也就是说,UI元素应当使用一些视觉和行为属性来表示它支持的操作——例如按钮应当做成看起来就可以被按下的样子、Slider应该有个槽槽来表示它可以被滑动,等等——而不是使用手型指针来提示这些操作。例子就像上面给出的Windows 资源管理器,以及QtCreator的侧边栏。

但为什么我不反感在普通网页中大量使用手型指针?

这里我也没有想的很清楚,可能的原因有:①在使用浏览器浏览网页时,我不期待网页会有Native的Look’n’Feel;②习惯了!

不过,我觉得主要的原因还是由于网页与客户端程序存在区别:

网页的本质是一篇文档。当我浏览一篇网页时,跟看一本杂志、看一本书很像。因此,网页上的交互组件应该优先与文档的整体风格保持一致,而不是优先显得“affordable”(不知道怎么翻译,可操作性?)。一个看起来就能够按下的按钮,且不说风格问题,更有可能喧宾夺主。所以我们可以弱化这些元素的affordance,而使用手型指针来增强操作提示。(说实话,很多Web UI Component的按钮,我就没什么按下去的欲望。)

然而客户端程序不是文档,尽管我们依然使用Web的技术来构建它,但它不是一篇文档。它的功能性更加重要,各类UI元素就是界面的主体。所以应该把各类UI元素在视觉上就设计得足够affordable,而不是去借助手型指针。上面贴出的VSCode中的各种button,有的甚至连hover效果都没有!

正例:Github Desktop

Github Desktop是我想举出的正例之一。它的下拉菜单、按钮、列表等等,全部使用普通鼠标指针,使用起来非常愉快:

结尾

其实除了手型指针这个问题之外,有些App还有一些小地方不够Native,比如Microsoft Teams中的一些图标存在延迟加载问题。在用Web技术做移动App时,大家都在往Native Look’n’Feel 靠拢;为什么到了Desktop,却不在意这些体验呢?

最后如果大家知道哪个UI库不滥用手型指针的,请推荐一个……

附:可以参考的其他讨论

https://ux.stackexchange.com/questions/105024/why-dont-button-html-elements-have-a-css-cursor-pointer-by-default

在 Medium.com 上查看

组织哈工大技术兴趣讨论班的心路历程

去年的秋季学期还没开始的时候,我就在考虑技术兴趣讨论班计划——让对某方面技术感兴趣的同学聚集在一起,定期轮流做一些分享。一晃眼今年都快过完了,想着把去年一年的经过和想法整理一下,如果将来有人还想办一办类似的活动的话,这就算作是宝贵的经验吧。

1

其实这是一连串的事件。办技术讨论班并不是我突然想做的,还有很多前戏。最最前的事件,大概就是IBM技术俱乐部暂停招新,随后 run.hit.edu.cn 镜像站又挂掉了。每次打开USTC Mirror,打开TUNA,心里面总是有一点嫉妒,现在仍旧如此。哈工大坐落在东北荒凉之地,哪有机会去参加Ubuntu Release Party,甚至连一个小小的镜像站都倒了。

所以,我想活跃一下校园里面的技术氛围。其实计算机和软件学院有很多的技术社团,也有很多人技术很不错的,但我总觉得差了点东西。

最开始大概在2015年,我想办一个技术社区。http://techo.io ,现在已经凉了,大家可以上去再给它续一续命。虽说当时本来就没有抱着办成功的态度去做,但还是有一点点的遗憾。把techo搭起来了之后,刚好学校Z老师有意向办一个技术咖啡馆,交给了我的基友们去做。线上线下联动,看起来甚至还有那么点希望。场地有了,线上讨论区有了,我甚至有着很多美好的设想:给各个技术社团提供线上讨论板块,线下活动场地,技术氛围搞起来啊。

2016年年中,线上论坛已经搭的差不多,咖啡店也已经装修完毕了。

2

每年的六七八月份,正是高考结束,考生撕书相庆的时节。高考结束之后就是填报志愿了,大概六月底七月初成绩公布,学生们未来的去处也就大致确定了。此时,多半会诞生新的新生群,诸如“2017级哈工大新生1群”。早就计划了要搞一点事情的我,必然要混进新生群去,因为之后的学院群的创建(比如“哈工大2017计算机”之类)多半可以从这样的新生群里面得知,同时还可以先混个脸熟,将来搞事情的时候不会冷场。

当然,技术兴趣讨论班不仅仅面向大一新生,但这是需要对大一新生做出的额外准备。因为大二大三大四这几届,在他们入学的时候,我已经基本做完了这些操作。

那一段时间,在跟学弟学妹们扯皮的同时,我也在思考讨论班究竟以什么样的形式来进行活动。哈工大大一的所有学生都在黄河路的二校区,大二以上年级的学生基本都在西大直街的一校区。一起活动吗?还是分开活动?让高年级的学生直接给大一同学开小灶吗?还是内部轮流分享?能做到每周都有东西可以拿来分享吗?

最终,我采取的方案是:讲书。比如我对CSAPP感兴趣,那么看看大家谁还对CSAPP感兴趣,组成一个“CSAPP讨论班”,大家一起来学,每周安排一个人将书中的一章或半章。不限制校区,地点安排服从多数人方便的标准。如果有人对SICP感兴趣,那么就组成另一个“SICP讨论班”,等等。

继续阅读

Codeforces 869C The Intriguing Obsession

这是一道动态规划题。

首先来分析:题干中对于桥梁的限制,其实是在说:某个点不能与自己集合中的点相连,也不能同时与同一个集合中的两个点相连,即题目下方示例2中那两种不合法的连接方法。也就是说,对于红色点A来说,它可以与蓝色集合中至多一个点相连;同时,也可以与紫色集合中至多一个点相连。它是否与蓝色集合中的点相连并不影响其与紫色集合的点相连,即桥梁搭建的全部情况,应该等于红蓝相连的情况数×红紫相连的情况数×蓝紫相连的情况数。因为不论红紫、蓝紫之间如何相连,红蓝之间相连的情况数跟它们均无关。

现在我们把问题由三个集合简化到了两个集合。这两个集合之间的点相连接需要满足刚才那两个条件:

  • 集合内部的点不能相连
  • 集合A中的一个点要么与集合B中的一个点相连,要么就不连接集合B中的任何点

第二个条件其实就是动态规划的转移方程了。对于集合A中的第I个点:

  • 第一种情况,它不跟集合B中的任何一个点相连。此时即变为一个子问题:集合A中的i-1个点与集合B中的j个点搭建桥梁;
  • 第二种情况,他跟集合B中的某一个点相连。连接方法共有j种,连接后变为另一个子问题:集合A中剩余的i-1个点与集合B中剩余的j-1个点相连。

因此,转移方程为:

dp[i, j] = dp[i - 1, j] + dp[i - 1, j - 1] * j

dp[i, j]代表的就是当集合A中有i个点、集合B中有j个点时,桥梁的连接方法。

由于只用到了i – 1,因此只需要存储一行即可。不过由于每个集合中的点数最多只有5000,即便用long存也大概只有200MB,内存勉强够用。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _869C
{
    class Program
    {
        public static void Swap<T>(ref T lhs, ref T rhs)
        {
            T temp = lhs;
            lhs = rhs;
            rhs = temp;
        }

        static void Main(string[] args)
        {
            int[] tmp = Console.ReadLine().Split().Select(i => int.Parse(i)).ToArray();
            if (tmp[1] < tmp[0])
            {
                Swap(ref tmp[0], ref tmp[1]);
            }
            if (tmp[2] < tmp[0])
            {
                Swap(ref tmp[0], ref tmp[2]);
            }

            long[,] dp = new long[tmp[1] + 1, tmp[2] + 1];
            for (int i = 0; i <= tmp[1]; i++)
            {
                for (int j = 0; j <= tmp[2]; j++)
                {
                    if (i == 0 || j == 0)
                    {
                        dp[i, j] = 1;
                    }
                    else
                    {
                        dp[i, j] = (dp[i - 1, j] + dp[i - 1, j - 1] * j) % 998244353;
                    }
                }
            }

            long r = (((dp[tmp[1], tmp[0]] * dp[tmp[1], tmp[2]]) % 998244353) * dp[tmp[0], tmp[2]]) % 998244353;
            Console.WriteLine(r);
        }
    }
}

Codeforces 869A The Artful Expedient

暴力即可

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _869A
{
    class Program
    {
        static void Main(string[] args)
        {
            int n = int.Parse(Console.ReadLine());
            int[] x = Console.ReadLine().Split().Select(i => int.Parse(i)).ToArray();
            int[] y = Console.ReadLine().Split().Select(i => int.Parse(i)).ToArray();

            ISet<int> xset = new HashSet<int>(x);
            ISet<int> yset = new HashSet<int>(y);

            int sum = 0;

            foreach (var xe in xset)
            {
                foreach (var ye in yset)
                {
                    int r = xe ^ ye;
                    if (xset.Contains(r) || yset.Contains(r))
                    {
                        sum++;
                    }
                }
            }

            if (sum % 2 == 0)
            {
                Console.WriteLine("Karen");
            }
            else
            {
                Console.WriteLine("Koyomi");
            }
        }
    }
}

Codeforces 869B The Eternal Immortality

挨个乘一下,如果结果是0了跳出即可。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _869B
{
    class Program
    {
        static void Main(string[] args)
        {
            long[] tmp = Console.ReadLine().Split().Select(i => long.Parse(i)).ToArray();
            long a = tmp[0];
            long b = tmp[1];
            long r = 1;
            while(b > a)
            {
                r *= b;
                r = r % 10;
                if (r == 0)
                {
                    break;
                }
                b--;
            }
            Console.WriteLine(r);
        }
    }
}

Codeforces 2A Winner

记录一下序列即可

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _2A
{
    class Program
    {
        static void Main(string[] args)
        {
            int n = int.Parse(Console.ReadLine());
            IDictionary<string, List<Tuple<int, int>>> score = new Dictionary<string, List<Tuple<int, int>>>();

            for (int i = 0; i < n; i++)
            {
                string[] tmp = Console.ReadLine().Split();
                int s = int.Parse(tmp[1]);
                if (!score.Keys.Contains(tmp[0]))
                {
                    score.Add(new KeyValuePair<string, List<Tuple<int, int>>>(tmp[0], new List<Tuple<int, int>>()));
                }
                else
                {
                    s += score[tmp[0]].Last().Item1;
                }
                score[tmp[0]].Add(new Tuple<int, int>(s, i));
            }
            List<string> names = new List<string>();
            int finalScore = 0;
            foreach (var item in score)
            {
                if (names.Count == 0)
                {
                    names.Add(item.Key);
                    finalScore = item.Value.Last().Item1;
                }
                else
                {
                    if (item.Value.Last().Item1 > finalScore)
                    {
                        names.Clear();
                        names.Add(item.Key);
                        finalScore = item.Value.Last().Item1;
                    }
                    else if (item.Value.Last().Item1 == finalScore)
                    {
                        names.Add(item.Key);
                    }
                }
            }

            string maxName = "";
            if (names.Count > 1)
            {
                int earliestRound = 99999;
                foreach (var name in names)
                {
                    for (int i = 0; i < score[name].Count; i++)
                    {
                        if (score[name][i].Item1 >= finalScore && score[name][i].Item2 < earliestRound)
                        {
                            maxName = name;
                            earliestRound = score[name][i].Item2;
                            break;
                        }
                    }
                }
            }
            else
            {
                maxName = names[0];
            }

            Console.WriteLine(maxName);
        }
    }
}

Codeforces 852F Product transformation

根据题意首先整理一下规律:

假如原始数组为 2 2 2 2,那么变换几次之后的数组分别为:

 

2 2 2 2
4 4 4 2
16 16 8 2
256 128 16 2

我们只关心指数,每个数的指数为:

1 1 1 1
2 2 2 1
4 4 3 1
8 7 4 1

这样原来每两个数相乘,就变成了每两个数相加。为了让规律再明显一点,我们计算前后两个数的差:

0 0 0
0 0 1
0 1 1
1 2 1

可以发现这是杨辉三角。杨辉三角第m行的第i个数字就是\(C_m^i\)因此数组从右往左第k个数的指数就是\(Sigma_{i=1}^k C_m^i\),其中m是变换的次数。下面的任务就是如何计算出每一个\(C_m^i\)。

计算mCi时,分子或分母相乘时很容易造成溢出。不过,由于题目只要求计算对某个Q取模的结果,我们可以充分利用这一点。不过需要注意的是,mCi这里并不是对Q取模,而应该对\(\phi\)取模。考虑到\(a^0 === 1\),即\(a^0 mod Q == 1\),\(\phi\)是下一次对Q取模等于一的幂——即余数每\(\phi\)次循环一遍。要计算\(\phi\),只需要不断计算a的幂取模的结果即可,这些结果后面还用得上;如果发现取模为1了,就可以停止计算。

回到计算mCi的问题上,分子的计算可以利用\(ab mod c = (a mod c)(b mod c) mod c\)计算,比较难办的是分母如何计算。分母的计算可以利用费马小定理,得到\(a^{-1} = a^{p-2} mod p\),将\(a^{-1}\)的计算改为计算\(a^{p-2} mod p\)。利用快速幂取模即可。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _852F
{
    class Program
    {
        static long exp_mod(long a, long b, int mod)
        {
            if (b == 0)
                return 1;
            if (b == 1)
                return a % mod;
            long t = exp_mod(a, b / 2, mod);
            t = (t * t) % mod;

            if (b % 2 == 1)
                t = (t * a) % mod;
            return t;
        }

        static long[] C(int n, int q)
        {
            int k = n / 2;
            if (n % 2 == 0)
            {
                k--;
            }
            List<long> arr = new List<long>();
            arr.Add(1);
            long a = 1;
            int n2 = n;
            for (int i = 1; i <= k; i++)
            {
                a = (a * n2) % q;
                a = (a * exp_mod(i, q-2, q)) % q;
                n2--;
                arr.Add(a);
            }

            List<long> arr2 = ((IEnumerable<long>)arr).Reverse().ToList();

            if (n % 2 == 0)
            {
                a = (a * n2) % q;
                a = (a * exp_mod(k + 1, q - 2, q)) % q;
                arr.Add(a);
            }

            arr.AddRange(arr2);

            return arr.ToArray();
        }

        static void Main(string[] args)
        {
            int[] t = Console.ReadLine().Split().Select(i => int.Parse(i)).ToArray();
            int n = t[0];
            int m = t[1];
            int a = t[2];
            int q = t[3];

            List<long> pow = new List<long>();
            pow.Add(1);
            long p = 1;
            while (true)
            {
                p = (p * (a % q)) % q;
                if (p != 1)
                {
                    pow.Add(p);
                }
                else
                {
                    break;
                }
            }

            long[] c = C(m, pow.Count);

            List<long> arr = new List<long>();
            arr.Add(a % q);
            int tmp = 1;
            for (int i = 1; i < n; i++)
            {
                if (i <= m)
                {
                    tmp += (int)c[i];
                    tmp %= pow.Count;
                }
                arr.Add(pow[tmp]);
            }

            arr.Reverse();

            Console.WriteLine(string.Join(" ", arr));
        }
    }
}