引言清朝，作为中国历史上最后一个封建王朝，从1616年努尔哈赤建立后金政权开始，到1912年末代皇帝宣统帝退位，历经近300年的风雨。从一统天下的辉煌到内忧外患的衰落，清朝的兴衰历程不仅是中国历史的重要篇章，也是世界历史中一段波澜壮阔的经历。清朝的建立与发展，无疑是中国封建王朝历史中的一个重要转折点。努尔哈赤和他之后的皇帝们通过军事征服、政治谋略和文化融合，成功地将一个起初弱小的女真部落发展为一个横跨东北、华北、华中以及西北广大地区的庞大帝国。清朝在康熙、雍正、乾隆三位帝王的治理下，达到顶峰，创造了中国历史上著名的“康乾盛世”。然而，随着统治时间的推移，清朝也逐渐暴露出其自身的弱点和问题。自道光帝时期开始，内忧外患接踵而至。鸦片战争、太平天国运动、以及列强的干涉，使得清朝政府疲于应对，国家财政状况恶化，社会矛盾也日益尖锐。清末的改革尝试，如光绪帝的“百日维新”，也因保守势力的强大阻力而未能成功。清朝的十二位皇帝，每一位都在这个历史的舞台上留下了自己的印迹。他们或开创基业，或励精图治，或力挽狂澜，或无奈收场。通过他们的治国策略、政策措施以及个人性格，我们可以更全面地理解清朝这个封建王朝的全貌，以及其从辉煌走向衰落的必然性。本文将以清朝十二位皇帝为线索，深入探讨他们的生平及其治国理念，揭示他们在清朝历史中的重要性和影响力。希望通过这段历史的回顾，能够为读者提供一些有益的启示，帮助我们更好地理解历史，并从中汲取经验教训。一、清太祖努尔哈赤：开疆拓土的奠基者清朝的开创者努尔哈赤（1559-1626），其出身于建州女真部落，祖籍为今辽宁省新宾县赫图阿拉村。他的家族世代为建州女真部落的领袖，在一个相对分散、纷争不断的环境中成长。努尔哈赤幼时丧父，家庭背景并不显赫，但他凭借卓越的军事才能和政治智慧，逐步崛起为女真各部的领袖。努尔哈赤早年即展示出卓越的军事和政治才能。他通过征战、联姻和外交手段，逐步统一了建州女真。在1583年，努尔哈赤发动一系列攻势，击败了邻近的女真部落以及汉族军队，不断扩大其统治范围。努尔哈赤在1593年的九部联军之战中，以少胜多，彻底巩固了自己在女真各部中的霸主地位。努尔哈赤于1616年建立后金，定都赫图阿拉，正式称汗。这标志着女真部落从一个松散的联盟逐渐转变为一个中央集权的国家。为了进一步巩固其统治，努尔哈赤进行了多项改革。他实行八旗制度，将女真各部编入八个军事单位中，八旗不仅是军事组织，也是行政管理单位，有效提高了军事和行政效率。此外，他还注重发展农业和手工业，鼓励开垦荒地，增加粮食生产，以支持日益扩张的军事需求。在外交方面，努尔哈赤积极与明朝、大明周边的蒙古各部和朝鲜进行互动，既有征战，也有联姻和贸易。他的战略目标不仅限于统一女真各部，更是要与明朝争夺东北的统治权。1618年，努尔哈赤发布“七大恨”檄文，正式对明朝宣战，开启了长达数十年的明清战争。在努尔哈赤的领导下，后金军队屡次在战场上击败明军，逐步控制了辽东大片地区，为后来的入关奠定了基础。努尔哈赤的历史功绩不仅在于他建立了后金，统一了女真各部，更在于他为大清帝国的建立和发展奠定了坚实基础。他所创立的八旗制度、军事战略和行政管理模式，被其后继者保留并发扬光大，对清朝的巩固和扩张起到了至关重要的作用。努尔哈赤去世后，其子皇太极继位，继续推动后金向大清帝国的转变，最终完成了对中原的统一。努尔哈赤的一生充满了战斗与智慧，他那不屈不挠的精神和卓越的领导才能，使他成为一位名副其实的开疆拓土的奠基者，为清朝的辉煌奠定了基石。二、清太宗皇太极：缔造大清帝国的基础皇太极，原名爱新觉罗·皇太极，是清朝的第二位皇帝，也是清朝的奠基者之一。作为努尔哈赤的第八子，皇太极在其父去世后继位，继续推进满洲的统一和扩张。他在位期间，不仅从后金转变为大清，更是在经济、军事和文化方面进行了一系列的改革，为大清帝国的稳固和发展奠定了坚实的基础。从后金到大清在努尔哈赤去世后，皇太极继位于1626年。当时的后金虽然已经占据了辽东大部分地区，但内部仍需进一步整合，外部也面临明朝的强大压力。皇太极深知，要想建立一个持久稳固的政权，仅靠武力是不够的，还需要在制度和文化上进行深远的变革。1636年，皇太极正式改国号为“大清”，并自称“皇帝”，宣告了一个全新帝国的诞生。这一举措不仅增强了后金统治集团的凝聚力，还为未来的入关和统一全国做好了准备。经济改革为了增强国家的经济实力，皇太极推行了一系列重大改革。首先，他倡导农业生产，鼓励满洲人学习汉族的农业技术，提高粮食产量，以解决军粮供应问题。其次，他重视商业发展，尤其是东北地区的皮毛、药材等特产贸易，通过与蒙古、西伯利亚等地的互市，增加了清朝的经济收入。此外，皇太极还注重财政管理，通过严格的税收制度，确保了国家财政的稳定。军事改革在军事方面，皇太极延续了努尔哈赤的八旗制度，但进行了更加系统和科学的改进。他进一步完善了八旗的组织结构，增强了八旗军的战斗力和机动性。同时，他还引进了汉族军事人才，吸收了汉族的先进军事技术和战术，提升了清军的整体战斗力。皇太极还善于利用外交手段，通过与蒙古各部结盟，扩大了清朝的势力范围。文化改革文化方面，皇太极推行了一系列政策，旨在增强满洲人和汉族之间的文化融合。他提倡学习汉文化，推广汉语和汉字，提高满洲贵族的文化素养。他还注重历史和文化的传承，命令编纂了《满文老档》等重要历史文献，保存了满洲的历史记忆。同时，他尊重汉族的儒家文化，积极吸纳汉族士人入仕，促进了满汉文化的交流与融合。对大清帝国的影响皇太极的一系列改革措施，为大清帝国奠定了坚实的基础。他不仅加强了清朝的统治能力，提高了经济和军事实力，还促进了满汉文化的融合，为清朝未来的繁荣和稳定打下了良好的基础。虽然皇太极在位时间不长，但他的改革精神和治国理念，对后来的康熙、雍正、乾隆等皇帝产生了深远的影响。皇太极作为清朝的奠基者之一，他的贡献在历史上得到了高度评价。他不仅成功地将后金转变为大清，还通过一系列的改革措施，为大清帝国的崛起和发展奠定了坚实的基础。这些改革不仅增强了清朝的内部凝聚力，也提高了清朝在国际上的地位，为清朝的繁荣昌盛创造了有利条件。三、顺治帝：幼主治国与入关之战顺治帝爱新觉罗·福临，是清朝第三位皇帝，也是顺治朝的开创者。顺治帝年幼即位，成为历史上一个重要的幼主治国的典范，并在清朝入关、定都北京这一重大历史事件中扮演了关键角色。顺治帝的即位与辅政四大臣1643年，皇太极去世，年仅六岁的福临在多尔衮等人的拥立下登基，成为清朝的第三位皇帝，年号顺治。在福临即位之初，由于他年纪尚幼，国政由辅政四大臣共同治理，即睿亲王多尔衮、郑亲王济尔哈朗、肃亲王豪格和成亲王萨哈廉。辅政四大臣中，多尔衮权力最大，实际上成为了清朝的实际统治者。清朝入关与定都北京顺治元年（1644年），明朝在李自成领导的农民起义军的冲击下土崩瓦解。为稳定北方局势，多尔衮率领清军入关，并与明朝降将吴三桂合作，在山海关战胜李自成。随后，清军迅速进驻北京，正式宣告清朝入主中原。1644年10月，顺治帝迁都北京，标志着清朝正式成为全国性的统治王朝。顺治帝的个人性格与治国理念顺治帝虽然即位时年幼，但在辅政大臣们的辅佐下，逐渐显示出其治国的才干和智慧。他性格温和，聪颖好学，对佛教有深厚的兴趣。据史书记载，顺治帝在位期间，注重宽刑恤民，推广儒家思想，努力恢复和发展经济。顺治帝对汉文化有着浓厚的兴趣，重视与汉族士大夫的合作，积极吸纳汉族官员参与政务。为了巩固统治，他采取了一系列稳健的政策，力图缓和满汉矛盾，建设一个多民族统一的国家。结语顺治帝虽然在位时间较短，但他的即位和治国经历对清朝的稳定和发展产生了深远影响。他在幼年即位，依靠辅政大臣的支持，完成了清朝入关和定都北京的重大历史任务，为清朝的后续发展奠定了坚实基础。顺治帝的治国理念和个人性格，体现了清朝初期对于国家统一、民族融合和社会稳定的追求。四、康熙帝：智勇双全的千古一帝康熙帝（1654-1722），名爱新觉罗·玄烨，是清朝第四位皇帝，也是中国历史上在位时间最长的一位皇帝。他在位61年，既是清朝的奠基者之一，又是其鼎盛时期的缔造者之一。康熙帝不仅在政治、军事、文化等领域取得了卓越成就，其个人的智慧与魅力也为后世所称颂。康熙帝即位之初，年仅八岁，辅政大臣鳌拜专权，朝廷内忧外患。康熙十四年（1675年），康熙帝以果断的手段除掉了鳌拜，为清朝的中兴打下了基础。康熙年间最为著名的事迹之一当属平定三藩之乱。三藩之乱是清初的一次重大内乱，始于康熙十二年（1673年），由吴三桂、尚可喜、耿精忠等三位藩王联合发动，对清朝的统一构成了严重威胁。康熙帝以卓越的军事才能和坚定的决心，经过八年的艰苦奋战，最终平定了三藩之乱，巩固了清朝的统一。康熙帝还成功收复了台湾。康熙二十二年（1683年），康熙帝派遣施琅率军进攻郑氏政权，经过激战，最终攻破台湾，结束了郑氏政权对台湾的统治，台湾重新纳入清朝版图。康熙帝在位期间，还积极应对外部威胁，特别是来自沙俄的侵扰。康熙二十七年（1688年），清军与沙俄军队在雅克萨展开激战，经过多次战斗，最终在康熙三十一年（1691年）的尼布楚条约中，清朝与沙俄划定了边界，保障了边疆的安宁。康熙帝不仅在军事上取得辉煌成就，他在文化领域的贡献也同样卓越。他多次南巡江南，亲自考察民间疾苦，提倡并实施了许多有利于百姓的政策。此外，他还主持编纂了《康熙字典》，促进了中华文化的传承与发展。康熙帝政治智慧的突出表现还在于他的宽容与开放。他重视汉族知识分子的作用，任用了一大批优秀的汉族官员，如张廷玉、李光地等，为清朝的稳定与发展作出了重要贡献。康熙帝在位期间，清朝经济繁荣，社会安定，文化昌盛，被誉为“康乾盛世”的开端。康熙帝以其智勇双全的形象，成为千古一帝，留下了深远的历史影响。康熙帝不仅是清朝的中兴之主，也是中华民族历史上一位杰出的统治者。他的智慧与魅力，至今仍为后人所敬仰。五、雍正帝：勤政爱民的铁腕皇帝清世宗爱新觉罗胤禛，即雍正帝（1678-1735年），是清朝第五位皇帝。他在位期间（1722-1735年），以其卓越的改革能力和铁腕的治国策略，奠定了清朝中兴的基础。雍正帝以其严谨、务实和勤政爱民的形象，被誉为清朝历史上最为精明能干的皇帝之一。摊丁入亩：减轻人民负担雍正帝在位期间，推行了一项重要的税制改革——摊丁入亩。这一改革将原本对丁（即成年男子）征收的丁银并入土地税，按土地数量统一征收。摊丁入亩的实施，减少了对个体农民的人头税负担，确保了税收的公平性，有效缓解了人民的负担。这一措施不仅有利于农业生产的稳定发展，也促进了社会的安定。火耗归公：整顿财政为了整顿财政，雍正帝推行了火耗归公的政策。所谓火耗，是指在税收过程中因为各级官员贪污腐败而产生的额外费用。雍正帝下令将这些费用统一归公，由国家统一管理，杜绝了地方官员中饱私囊的现象。火耗归公政策的实施，大大提升了财政收入的透明度和有效性，确保了国家财政的稳定。铁腕治官：严惩贪腐雍正帝对官员贪腐行为采取了严厉的打击措施。他设立了巡抚、总督等调查贪腐的专职机构，实行严格的监察制度，凡是发现有贪污腐败行为的官员，无论职位高低，均严惩不贷。雍正帝强调以身作则，要求官员勤政廉洁，树立了良好的官场风气。重视人才：用人唯才雍正帝在用人方面，主张唯才是举。他通过科举考试选拔人才，并注重个人才干和品行。他大胆启用有能力的官员，不论出身背景，只要有才能，均予以重用。这一用人政策，提高了官僚队伍的整体素质，为国家的繁荣稳定提供了有力的人才支持。勤政爱民：以身作则雍正帝以勤政爱民著称。他每日处理政务，坚持批阅奏折，亲自听取臣下的汇报，以了解民情，解决问题。他不仅在宫中勤政，还多次微服私访，亲自了解民间疾苦，体察民意。雍正帝的勤政精神，赢得了百姓的尊敬和爱戴，也增强了政府的执行力。雍正帝在位期间，通过一系列改革措施，有效整顿了官场风气，稳定了财政，实现了国家的繁荣与稳定。他的勤政爱民形象和铁腕治国策略，为后来的乾隆盛世打下了坚实的基础，成为清朝历史上不可或缺的重要一环。六、乾隆帝：盛世与危机并存的多面君主乾隆帝（1711-1799），名爱新觉罗·弘历，是清朝的第六位皇帝，在位时间长达六十年（1735-1796）。乾隆帝在位期间，被誉为中国历史上的“康乾盛世”之巅，然而，在这繁华盛世的背后，危机也悄然滋生。治国政策乾隆帝继承了康熙、雍正两代皇帝的治国理念，继续推行一系列经济、军事和文教政策，以巩固清朝的统治。乾隆帝重视农业，推行“垦荒政策”，鼓励开垦荒地，增加粮食产量；同时加强对边疆的治理，通过设立驻防制度，维护边疆安宁。在他的治理下，清朝的疆域达到历史最大，南至南海，北至西伯利亚，东至库页岛，西至帕米尔高原。文化成就乾隆帝是一个极具文化修养的皇帝，他热衷于文学、书画和收藏。他亲自主持编纂了《四库全书》，这部巨著汇集了中国古代文化的精华，堪称文化工程的巅峰。此外，他还大力提倡尊孔读经，推崇儒学，修建孔庙，提升了儒家文化的地位。对外征战乾隆帝在位期间，对外进行了多次军事行动，以巩固边疆和扩大疆域。最著名的包括平定准噶尔叛乱、平定大小金川之战以及征伐廓尔喀等。这些战争不仅巩固了清朝的边疆领土，还增强了中央对地方的控制力，使清朝的疆域达到了空前的广阔。官僚腐败尽管乾隆帝在位期间清朝的经济、文化和军事都达到了新的高度，但盛世背后也隐藏着深刻的隐忧。官僚腐败问题尤为严重，乾隆晚年时期，官场积弊丛生，贪污腐败之风日盛。最为典型的例子就是和珅，这位权臣在乾隆帝的宠信下，掌握了大量权力，积累了巨额财富，成为清代历史上最为典型的贪官。财政危机乾隆帝在位期间，清朝的财政状况也逐渐陷入困境。乾隆晚年，因多次对外用兵和大量兴建宫殿园林，财政开支庞大。乾隆六下江南，挥霍无度，加之官员贪腐成风，导致国库空虚，财政危机逐渐显现。这不仅削弱了国家的财政基础，也为后来的社会矛盾和动荡埋下了隐患。乾隆帝在位期间的成就与危机相伴而生，他的治国政策和文化成就无疑为大清帝国增光添彩，但官僚腐败和财政危机也为清朝的衰落埋下了伏笔。乾隆盛世的繁华表象下，隐藏的危机逐渐积累，最终导致了清朝在道光、咸丰时期的加速衰败。乾隆帝的多面性，既是盛世的缔造者，也为后来的危机埋下伏笔，成为后世历史学家研究的经典案例。七、嘉庆帝：平定白莲教起义的皇帝嘉庆帝（即爱新觉罗·颙琰），是清朝的第七位皇帝，在位期间（1796年-1820年）面临着诸多挑战，其中最为显著的便是白莲教起义。这场起义不仅对清朝的统治构成重大威胁，更揭示了清朝政治、经济和社会系统的深层次危机。嘉庆帝继位后的首要任务便是应对白莲教起义。白莲教起义爆发于乾隆末年，起初是民间反抗赋税和苛政的局部斗争，但随着社会矛盾的激化，迅速发展为大规模的武装叛乱。嘉庆帝登基后，立即采取了一系列军事和政治措施，以平息这场动荡。他任命忠诚且有能力的官员，如富察·福康安和海兰察等，将他们派往起义前线指挥作战。同时，嘉庆帝还加强了对地方官员的监督，整肃吏治，试图通过改革来恢复政府效能。经过数年的艰苦战斗，清军终于在1804年成功平定了白莲教起义。嘉庆帝因此获得了一定的声望，但这场战乱也暴露了清朝统治的脆弱。战争导致了大量的人口流离失所，农田荒芜，财政损失严重。嘉庆帝意识到，单靠镇压无法从根本上解决问题，必须进行一系列改革以恢复社会秩序和经济发展。在面对腐败和财政困境时，嘉庆帝采取了一些重要措施。他重视吏治整顿，严惩贪官污吏，提倡节俭，减轻人民负担。然而，这些措施的效果并不显著，腐败依然严重，财政赤字的问题也没有得到根本解决。嘉庆帝在位期间，还发生了多次自然灾害，加剧了社会的不安定。与此同时，嘉庆帝也不得不面对清朝统治中期以来积累的深层次矛盾。清朝在康乾盛世达到鼎盛，但随着时间的推移，内部的积弊日益显现，统治阶层的腐化、官僚体系的低效以及财政危机，都成为制约清朝发展的主要问题。嘉庆帝虽然努力尝试改革，但由于改革力度不够，加之保守势力的强大反对，他的施政效果有限。总体而言，嘉庆帝在位期间尽管有一定的政治作为，但在应对清朝日益严重的内外困境时显得力不从心。白莲教起义的平定，固然是他的一大功绩，但也揭示了清朝统治结构的脆弱和社会矛盾的尖锐。嘉庆帝的治国经验和教训，为后来的清朝统治者提供了宝贵的参考，然而随着时间的推移，清朝的衰落趋势已不可避免地显现出来。八、道光帝：鸦片战争与大清的衰落道光帝（1821-1850年在位），本名旻宁，是清朝历史上在位时间较长的一位皇帝。他执政期间，清朝面临内忧外患，尤其是第一次鸦片战争（1839-1842年），对清朝的国力和尊严造成了极大的打击，标志着清朝由盛转衰的开始。1. 内政焦虑与民生困顿道光帝继位时，清朝已经显现出种种问题。人口膨胀、土地兼并、官员腐败和财政危机等问题层出不穷。为了应对这些问题，道光帝进行了一系列的改革措施。比如，整顿吏治，试图通过严惩贪官污吏来恢复朝廷的清明；改革税制，旨在增加国家财政收入。然而，这些措施效果有限，未能从根本上解决问题，反而在一定程度上加剧了社会矛盾。2. 鸦片泛滥与禁烟运动鸦片在道光年间迅速泛滥，成为社会的一大毒瘤。鸦片贸易不仅严重损害了国民健康，还导致白银大量外流，影响了国家的经济稳定。为了遏制鸦片的危害，道光帝在1839年任命林则徐为钦差大臣，展开了大规模的禁烟运动。林则徐采取了严厉的措施，销毁了大量鸦片，颇具成效。然而，这一禁烟行动触动了英国的利益，成为鸦片战争爆发的导火索。3. 第一次鸦片战争的爆发1839年，林则徐在广东虎门销烟，引发了英国的不满。1840年，英国以保护贸易为借口，发动了对清朝的战争。第一次鸦片战争的爆发，对清朝来说是一场措手不及的战争。清朝的军队虽然在数量上占优，但在武器装备和战术上远逊于英军。战争期间，清军屡战屡败，沿海城市相继陷落。4. 签订《南京条约》1842年，清朝不得不向英国妥协，签订了丧权辱国的《南京条约》。根据条约，清朝割让香港岛给英国，开放广州、厦门、福州、宁波、上海五个通商口岸，并支付2100万银元的赔款。《南京条约》不仅使清朝丧失了大量领土和财富，还开启了列强瓜分中国的序幕。5. 内外交困与晚年困局鸦片战争后，清朝的国际地位急剧下降，国内矛盾进一步激化。道光帝晚年面对内忧外患，力不从心。太平天国运动的萌芽，预示着更大的危机即将到来。道光帝虽然试图通过调整官员、整顿财政等措施来缓解危机，但未能从根本上扭转局面。道光帝在位期间的历史，是一段充满动荡和变革的时期。他在内政上虽有所作为，但未能根治积弊；在外交上则因鸦片战争而遭受重创。道光帝的执政，标志着清朝由盛世走向衰落的开端，对后世产生了深远的影响。九、咸丰帝：内忧外患的悲情皇帝咸丰帝，即爱新觉罗·奕詝，是清朝第九位皇帝，在位期间（1850-1861年）中国面临着内忧外患的严峻局面。咸丰帝的时代，既有捉襟见肘的内政，也有外部列强的紧逼，这是清朝由盛转衰的重要阶段。太平天国运动咸丰帝即位之初，国内面临着一场前所未有的内乱——太平天国运动。这一运动由洪秀全领导，起源于广西，迅速蔓延至全国多个省份。太平天国以“天朝田亩制度”为纲领，号召农民反抗清政府，建立一个平等、自由的理想社会。这场运动不仅严重削弱了清朝的统治基础，还给国家带来了极大的社会动荡和经济破坏。面对太平天国的强大势力，咸丰帝采取了一系列军事和政治措施，希望能够迅速平息动乱。他依靠曾国藩、李鸿章等地方实力派，组织起湘军和淮军，与太平军展开激烈的战斗。然而，战事持续多年，造成大量人员伤亡和财力消耗，即便最终清军取得了胜利，清政府的统治威望和国力已大不如前。第二次鸦片战争咸丰帝在位期间，清朝还遭遇了第二次鸦片战争（1856-1860年）。英法联军以“亚罗号事件”和“马神甫事件”为借口，再度侵略中国。战火燃遍广州、天津、北京等地，最后直逼皇宫紫禁城。面对强敌入侵，咸丰帝及其政府在军事上显得无力抵抗。咸丰帝本人也因战事一度逃往避暑山庄，避居热河。最终，清政府被迫签订了屈辱的《天津条约》和《北京条约》，割地赔款，开放更多通商口岸，这进一步加深了清朝的内忧外患。艰难抉择在内忧外患的局面下，咸丰帝面临着重重压力。太平天国运动和第二次鸦片战争使得国家内外交困，咸丰帝的统治权威和政府的公信力大大受损。他尝试通过任用一些有能力的官员，如曾国藩、左宗棠，来应对内乱和外敌，但成效有限。此外，咸丰帝在改革内政方面也有一定的尝试。例如，他推动了部分军事和财政改革，希望能够缓解社会矛盾，提高政府的行政效能。但由于清朝内部的保守势力强大，改革举步维艰，很多措施无法真正落实。悲情皇帝的结局咸丰帝在位十一年，内忧外患不断，身心俱疲，最终在1861年病逝于热河避暑山庄。他的去世标志着清朝进入更加动荡的时期，慈禧太后和恭亲王奕䜣等人掌权，开始了“同治中兴”的短暂时期。咸丰帝作为清朝的悲情皇帝，其统治时代是清朝由盛转衰的关键节点。在他的艰难抉择中，我们可以看到一个帝国的挣扎与无奈，以及一个帝王在内外交困中的无助与悲怆。他的经历为后人提供了深刻的历史教训：一个国家的强盛不仅依赖于统治者的决策，更需要社会各方面的共同努力和有效的制度保障。十、同治帝：短暂而动荡的少年皇帝同治帝爱新觉罗·载淳是清朝第十位皇帝，1861年即位，年仅六岁。作为咸丰帝的长子，他的登基象征着一个新的开始。然而，这一时期的清朝内外交困，局势动荡不安，同治帝的统治并未能带领国家走出困境。同治帝即位后，由于年幼，由其母慈禧太后和慈安太后垂帘听政，形成了“双后垂帘”的政治格局。慈禧太后，原名叶赫那拉氏，是咸丰帝的贵妃，后来的西太后。她以其精明、果断和强势的政治手腕，在清朝后期的政坛上占据了举足轻重的地位。慈禧太后在同治帝即位后，迅速掌握了朝政大权。她通过扶植亲信，控制了朝廷的中枢机构，排除异己，巩固了自己的权力地位。然而，慈禧太后的专权也引发了不少争议和反对。她的执政风格和决策方式在某种程度上加剧了清朝的内政问题和政治腐败。在同治帝统治的短暂时期，清朝的内忧外患日益加剧。国内，太平天国运动虽然已经基本被平定，但由此带来的社会动荡和经济损失依然未能完全恢复。此外，各地的地方权力也逐渐滋生了割据势力，中央政府的控制力逐渐削弱。外部，西方列强的侵略和压力依旧不减，清朝在国际关系上处于被动地位。同治帝本人在这种复杂的政治环境中表现得并不出色。他虽然曾试图亲政，但由于年轻和缺乏经验，再加上慈禧太后的强势干预，他的实际权力非常有限。1873年，同治帝亲政，但仅仅两年后，他在1875年因天花病逝，年仅19岁。同治帝的早逝让清朝的动荡局势更加雪上加霜。慈禧太后再度垂帘听政，扶持光绪帝即位，继续掌控朝政。虽然同治帝在位时间短暂，但这一时期的政治和社会动荡，对清朝后续的发展产生了深远影响。总的来说，同治帝的统治是清朝晚期的一个缩影，年轻的皇帝在动荡的局势下努力却无能为力，而背后的权力斗争和政治腐败则加剧了国家的困境。慈禧太后的垂帘听政在一定程度上维持了政权的稳定，但也为清朝的进一步衰落埋下了隐患。十一、光绪帝：改革与保守的矛盾体光绪帝（1871-1908年），名爱新觉罗·载湉，是清朝第十一位皇帝，年号光绪。他在位期间面临内忧外患，尤其是在中日甲午战争后，清朝的社会矛盾和内政问题愈发严峻。光绪帝作为年轻的皇帝，怀揣着振兴大清的雄心壮志，却在保守势力的掣肘下，经历了一场短暂而影响深远的政治改革——百日维新。生平简介光绪帝出生于同治十年（1871年），是咸丰帝的弟弟醇亲王奕譞的儿子，光绪四年（1875年）继位。光绪帝登基时年仅四岁，由慈禧太后垂帘听政，实权被慈禧掌控，光绪只是名义上的君主。百日维新运动光绪二十四年（1898年），适逢清朝在甲午战争中惨败，民族危机和社会矛盾空前加剧，急需变革图强。康有为、梁启超等维新派人物积极推动变法，光绪帝也希望通过改革来拯救清朝。在康有为的建议下，光绪帝决心进行一系列改革，史称“百日维新”或“戊戌变法”。从6月11日到9月21日的短短103天内，光绪帝颁布了百余道变法令，内容涵盖政治、经济、军事、文化等多个方面，主要措施包括：政治方面：裁撤冗官、改设内阁、开放言路，鼓励官员直言进谏；经济方面：推广实业、鼓励资本主义发展，如创办铁路、矿业等；军事方面：裁减冗兵、加强训练、引进西方军事技术；教育方面：废除科举、兴办新式学校、推行新学堂课程。改革的失败与挣扎然而，光绪帝的改革触动了以慈禧太后为代表的保守势力的利益，遭到强烈反对。慈禧太后联合权臣，发动了政变，史称“戊戌政变”。光绪帝被软禁在瀛台，百日维新宣告失败，康有为、梁启超等人被迫流亡海外，谭嗣同等六君子则被处死。矛盾与妥协光绪帝在改革中展现了年轻皇帝的锐意进取，但他的改革缺乏充分的政治基础和社会共识，最终未能成功。他在保守势力的束缚下，成了一个无实权的傀儡皇帝。光绪帝对国家的热爱和对变革的渴望与现实权力的钳制形成了强烈的矛盾。结语光绪帝的百日维新虽然短暂而失败，但它揭示了清朝迫切需要改革的现实，对中国近代史产生了深远影响。光绪帝在保守与改革之间的挣扎与妥协，反映了中国近代化过程中所面临的巨大挑战和阻力。从光绪帝的经历中，我们可以看到改革的复杂性和艰巨性，以及在推动社会进步过程中所需的智慧和勇气。十二、宣统帝：清朝的落幕与皇帝的晚年宣统帝爱新觉罗·溥仪，1906年出生，是清朝的最后一位皇帝。他在1908年年仅三岁时被选为皇帝，继位后成为了宣统帝。这一选择由慈禧太后和光绪帝的遗诏决定，溥仪的登基标志着清朝的终结已悄然临近。溥仪的幼年生活并不像传统意义上的帝王那样充满荣耀和权威。由于年幼，他的实际权力完全掌握在其身边的摄政王载沣以及其他大臣手中。溥仪的即位并未能改变清朝内外交困的局面，反而使这种局面更加复杂化。宫廷内部的权力斗争、外部列强不断加大的压力，使得清朝的统治岌岌可危。1911年，辛亥革命爆发，全国范围内的起义接连不断。革命军迅速攻占了多个省份，清朝的统治基础几乎土崩瓦解。面对内外交困的局势，清朝政府最终在1912年2月12日宣告退位，结束了267年的统治。溥仪被迫在紫禁城内签署了退位诏书，这也标志着中国两千多年封建帝制的终结，中华民国正式成立。退位后的溥仪仍然被允许居住在紫禁城内，保留了一定的皇室待遇，但实际上已不再拥有任何政治权力。1924年，冯玉祥发动北京政变，溥仪被驱逐出紫禁城，结束了他在紫禁城内的生活。之后的溥仪经历了曲折的人生。他在日本的支持下，于1934年成为伪满洲国的皇帝，改名康德，但实际上只是日本的傀儡。1945年，第二次世界大战结束，日本投降，伪满洲国随之灭亡，溥仪被苏联红军俘虏并押往苏联。1950年溥仪被遣返回国，后来成为中华人民共和国的公民。经过几年改造，溥仪逐渐适应了新社会的生活。他在1959年获得特赦，之后在北京市文史研究馆任职，并于1962年出版了自传《我的前半生》，讲述他从皇帝到普通公民的经历。溥仪的一生充满了跌宕起伏，从高高在上的皇帝到普通的公民，他的经历反映了中国近现代史的巨大变迁。作为清朝的最后一任皇帝，溥仪不仅是历史的见证者，也是历史的参与者，他的故事为我们了解那个动荡的时代提供了宝贵的视角。溥仪于1967年在北京去世，享年61岁，结束了他充满传奇色彩的一生。结语：清朝十二帝的历史遗产与现代启示清朝历经十二位皇帝，从努尔哈赤的开疆拓土到溥仪的退位，跨越了近三个世纪的时间。这段历史不仅深刻影响了中国的政治、经济、文化各个方面，其间的治国智慧与失误也为后人留下了宝贵的历史遗产和深刻的启示。首先，清朝的开国皇帝努尔哈赤和其子皇太极通过精心的军事谋略和政治手段，成功地统一了女真各部，奠定了清朝的初步基础。他们的成功不仅在于军事上的胜利，更重要的是通过有效的联盟策略和内部管理，建立了一个稳固的政治体系。现代社会可以从中汲取的经验是，国家的统一和稳定不仅仅依赖于军事力量，更需要灵活且有效的内部治理和政治智慧。康熙帝作为清朝最为人称道的皇帝之一，他在位期间通过平定三藩、收复台湾、南巡江南等一系列措施，巩固了国家的统一，增强了中央集权。他的治国理念和实际操作展示了一个成功的帝王应具备的政治智慧和领导能力。现代领导者可以从康熙帝的治国经验中学到，如何在复杂的政治环境中通过稳健的政策和果断的决策，提升国家的整体实力。然而，清朝的衰落也为后人提供了反思的空间。乾隆帝在位期间，虽然清朝达到了极盛，但也埋下了官僚腐败和财政危机的隐患。道光帝和咸丰帝在面对内忧外患时的应对策略失当，导致了鸦片战争和太平天国运动的爆发，这些事件加速了清朝的衰亡。现代社会应从中吸取的教训是，过度的权力集中和缺乏有效的监督机制，会导致系统性腐败和治理危机，必须通过制度建设和合理的权力分配来防范这种风险。光绪帝的百日维新和戊戌变法虽然最终失败，但也代表了清朝晚期改革派试图挽救国家的努力。这段历史告诉我们，改革的道路虽然艰难，但在国家面临重大危机时，唯有改革才能带来希望。现代社会在面对变革的过程中，应当有勇气打破旧有的桎梏，积极探索新的发展路径，同时需要避免急于求成，注重渐进式改革。最后，宣统帝的退位标志着清朝的终结，但清朝的历史遗产并未随之消散。清朝十二帝的治国智慧与失误，依然为中国历史留下了深刻的印记。现代社会在回顾这段历史时，不仅要看到其中的辉煌成就，也要深刻反思其失败的原因，从而在不断前行的道路上，避免重蹈覆辙，汲取智慧，创造更加辉煌的未来。备注：本文由专业博客写作助手生成（包括图片）。
引子：网络协议究竟是什么？我们每天通过互联网进行各种活动：购物、聊天、刷视频，但你是否想过这些数据是如何传输的？在这一章，我们将揭开网络协议的神秘面纱，用通俗易懂的语言解释其基本概念和重要性。首先，想象一下你在现实生活中给朋友打电话。你需要知道对方的电话号码，拨号，然后等待接通。这个过程中有很多规则：比如你要先拿起电话，听到拨号音后再拨号码，接通后要等对方接听才能开始对话。这一系列规则确保了你能顺利找到对方并进行交流。网络协议就像这些规则一样，是计算机网络中进行信息交换的标准和规范。它们定义了数据如何在网络上进行发送、接收和解释。没有这些协议，就像没有电话通话的规则，我们的计算机将无法相互理解和通信。你可能会问，为什么需要这么多协议？想象一下我们日常的网络活动：发送邮件、浏览网页、观看视频、在线购物……每一种活动都涉及不同类型的数据和不同的传输要求。为了满足这些多样化的需求，互联网发展出了多种网络协议，每种协议负责特定类型的通信。例如，浏览网页时最常用的协议是HTTP（超文本传输协议），它定义了浏览器和服务器之间交换网页内容的方式。发送和接收电子邮件则主要依靠SMTP（简单邮件传输协议）和POP3/IMAP（邮局协议/互联网消息访问协议）。这些协议各有其特定的功能和工作原理，但共同确保了数据在互联网中可以可靠地传输和接收。网络协议的重要性不言而喻，它们是互联网的基石，使得不同设备、不同平台之间能够互相通信，构建出一个全球性的网络。在接下来的章节中，我们将深入探讨一些关键的网络协议，用生动的例子和通俗的语言帮你理解这些看似复杂但实际上非常有趣的技术细节。准备好了吗？让我们一起踏上这段网络协议的探索之旅吧！第一章：TCP/IP - 守护互联网的隐形英雄在互联网的世界里，TCP/IP协议就像是交通规则，指引着数据包的正确方向。你可以把它们想象成邮局的工作人员，负责把信件（数据包）从一个地点送到另一个地点。尽管这些信件要经过错综复杂的路线，但TCP/IP协议确保它们最终能够准确无误地到达目的地。TCP：可靠的传输保障首先，让我们认识一下TCP（Transmission Control Protocol），它是保证数据可靠传输的关键人物。可以把TCP想象成一辆运送贵重包裹的快递车，它不仅要保证包裹送达，还要确保包裹的完整性。三次握手：确保连接稳固TCP最著名的特性之一是“三次握手”机制。想象一下你在打电话，第一次拨号就是请求连接，对方接听并回应表示可以接通，这就是第一次握手；你确认收到回应，并再次确认连接状态，这就是第二次握手；对方收到你的确认并最终确认连接成功，这就是第三次握手。通过这三次握手，TCP确保双方都准备好进行数据传输。分段和重传：确保数据完整TCP还负责将数据分成小片发送，并在接收端重新组装这些数据片。如果有任何数据片在传输过程中丢失，TCP会自动重传。好比邮局会分批次运送你的包裹，并在丢失时重新发送。IP：寻找最佳路径有了TCP的可靠传输保障，还需要IP（Internet Protocol）来负责找路。IP就像是地图导航系统，它决定数据包通过哪条路线才能最快最安全地到达目的地。IP地址：互联网的门牌号每一台连接到互联网的设备都有一个独一无二的IP地址，就像每个房子都有一个门牌号。IP地址分为IPv4和IPv6两种，IPv4是由四组数字组成的，例如192.168.1.1，而IPv6则是为了应对互联网设备数量爆发式增长而设计的，它由八组16进制数字组成，如2001:0db8:85a3:0000:0000:8a2e:0370:7334。路由选择：智能导航IP协议不仅负责地址管理，还要选择最佳路径。每个数据包在传输过程中会经过多个路由器，这些路由器就像是高速公路上的每个路口，决定数据包的下一跳。如果一个路口堵车了，IP协议会智能地选择其他可行路径，确保数据包能顺利到达。小故事：快递小哥的冒险假设你要给远方的朋友寄一份生日礼物，过程中你会先把礼物交给快递公司（就像数据由应用层交给传输层），快递公司会把大件礼物拆成多个小包裹（数据分段），并用快递单号（序列号）标记。每个小包裹会通过不同的路线送往朋友家（路由选择），快递公司会确保每一个包裹都安全送达。如果某个包裹丢失，快递公司会重新发送（重传机制）。最终，朋友会收到所有包裹并把它们重新组装成一份完整的礼物（数据重组）。通过这个小故事，你应该对TCP/IP协议有了更直观的理解。它们是互联网数据传输的无名英雄，确保我们每天都能顺畅地上网、通讯和获取信息。在接下来的章节中，我们将继续探索更多有趣且重要的网络协议，敬请期待！第二章：HTTP和HTTPS - 网站通信的语言在互联网世界中，HTTP和HTTPS就像是网站之间的语言翻译官，帮助我们与远程服务器进行有效的沟通。那么，这两位“翻译官”究竟是如何工作的呢？让我们通过一些有趣的对话和类比来揭开它们的神秘面纱。HTTP：无所不在的网络语言HTTP（HyperText Transfer Protocol）是互联网中最常用的通信协议，它为我们提供了访问网页的基础。可以把HTTP比作一位热情的邮差，每天穿梭在各个地址之间，传递信息。然而，这位邮差有个特点，他总是敞开着邮袋，任何人都可以看到他传递的信息。工作原理HTTP的工作原理其实很简单，就像是一个问答游戏。当你在浏览器中输入网址时，浏览器会向目标服务器发送一个请求（Request），然后服务器会返回一个响应（Response）。这个过程可以形象地比作：你在餐馆中点餐（请求）。服务员将菜单递交给厨房，并拿到菜品（服务器响应）。服务员将菜品送到你的桌上（浏览器展示页面）。这个过程看似简单，但其中包含了许多细节。例如，请求中包含了你想要访问的网页地址，而响应中则包含了网页的内容、状态码等信息。HTTPS：信息的保镖HTTPS（HyperText Transfer Protocol Secure）是HTTP的“进阶版”，也是现代互联网中不可或缺的一部分。如果说HTTP是敞开邮袋的邮差，那么HTTPS就是一位身披铠甲、手持盾牌的保镖，确保信息在传输过程中不会被窃取或篡改。HTTPS的工作原理HTTPS通过一种叫做SSL/TLS（Secure Sockets Layer / Transport Layer Security）的加密技术来保护数据的安全。具体来说，它在HTTP和TCP之间建立了一条加密通道，使得数据在传输过程中变得不可读。可以把HTTPS的工作机制想象成：你在银行的柜台办理业务（请求）。柜员通过玻璃对讲器与你沟通（建立加密通道）。你的信息通过加密通道传递，确保只有柜员能听到（数据加密）。柜员处理完毕后，通过同样的加密通道传回信息（服务器响应）。HTTP和HTTPS的区别虽然HTTP和HTTPS在表面上看起来非常相似，但它们之间有几个关键区别：安全性：HTTP是不加密的，任何人都可以截取和读取传输的数据；而HTTPS通过加密确保数据在传输过程中是安全的。端口号：HTTP通常使用80端口，而HTTPS使用443端口。证书认证：HTTPS需要SSL/TLS证书来验证服务器的身份，防止中间人攻击。为什么HTTPS很重要？在现代互联网时代，数据的安全性尤为重要。无论是在线购物、银行交易还是个人信息保护，HTTPS都起到了至关重要的作用。它确保了数据在传输过程中的完整性和私密性，让我们可以放心地在互联网上进行各种操作。小结通过对HTTP和HTTPS的简要介绍，我们了解到：HTTP是一种常见但不安全的通信协议。HTTPS通过加密技术为我们的数据提供了安全保障。HTTP和HTTPS就像是两位不同风格的邮差，一个简单直接但缺乏保护，另一个则严谨而安全。在接下来的章节中，我们将继续探讨其他重要的网络协议，揭开它们在互联网中的神秘角色。第三章：DNS - 互联网的电话簿你是否觉得每次输入网址就能找到对应网站的过程很神奇？其实，这背后有一个强大的系统在默默工作：DNS（域名系统）。想象一下，如果没有DNS，我们每次都要记住一串像192.168.1.1这样难记的IP地址，就像在没有电话簿的时代，你需要记住每个人的电话号码一样，简直要疯掉！让我们一起来揭秘DNS是如何将域名翻译为IP地址的。首先，域名（例如www.example.com）是我们访问网站时输入的地址，但计算机并不认识这些“人类友好的”名字。计算机只认识IP地址——这是一个唯一标识每台联网设备的数字。DNS的工作就是将容易记忆的域名转换成计算机能理解的IP地址。那么，这个转换过程是如何进行的呢？实际上，当你在浏览器中输入一个网址并按下回车键后，会发生一系列复杂且精妙的步骤。浏览器缓存：首先，浏览器会检查自身的缓存，看看是否已经有这个域名的IP地址。如果有，那么直接使用缓存中的IP地址。如果没有，继续下一步。操作系统缓存：浏览器找不到的话，会向操作系统请求，同样地，操作系统也有自己的DNS缓存。如果操作系统中也没有相关记录，接下来就要进行更广泛的搜索了。路由器缓存：接着，查询请求会被发送到路由器。路由器是家庭网络的“交通警察”，也有缓存记录。如果路由器也没有记录，才会真正进入互联网的DNS系统。递归DNS服务器：此时，查询请求到达ISP（互联网服务提供商）的DNS服务器。这些服务器被称为递归DNS服务器，它们会尝试找到域名的IP地址。如果这些服务器也没有记录，它们会代替你继续进行搜索。根DNS服务器：递归DNS服务器会首先向根DNS服务器发送请求。根服务器是DNS系统中的顶层节点，它们不会直接返回IP地址，但会告诉你下一步应该查询的顶级域名服务器。顶级域名服务器（TLD）：这些服务器负责特定类型的域名，例如.com、.org等。顶级域名服务器会告诉递归DNS服务器，下一步应该查询哪一个权威DNS服务器。权威DNS服务器：这是最终的决策者，存储着特定域名的IP地址。递归DNS服务器向权威DNS服务器发送请求，权威服务器会返回该域名对应的IP地址。当递归DNS服务器拿到IP地址后，它会将结果缓存起来，并返回给查询的客户端。从此刻起，浏览器就能使用这个IP地址来访问网站了。整个过程听起来复杂无比，但实际上只需几毫秒便能完成。DNS的工作原理像是一个层层递进的电话簿查找过程，从根节点一直到最具体的权威服务器，井然有序。下次你访问一个网站时，不妨想一想这个幕后英雄DNS，它默默地确保你每次都能顺利到达目的地。通过这些层层递进的步骤，DNS让互联网变得如此便捷而高效，就像是互联网的超级电话簿，不仅帮我们记住了所有网站的“电话号码”，还保证了我们能够快速、准确地找到所需的信息。第四章：SMTP、POP3和IMAP - 邮件传输的幕后功臣你有没有想过，当你点击“发送”按钮时，那封邮件到底经历了怎样的旅程呢？就像我们寄信需要邮递员，电子邮件的发送和接收也离不开几位幕后功臣：SMTP、POP3和IMAP。这些协议是如何协同工作的？让我们一起揭开它们的神秘面纱。SMTP - 邮件的快递员SMTP（Simple Mail Transfer Protocol），简单邮件传输协议，是负责发送邮件的协议。想象一下，它就像电子邮件世界里的快递员。当你点击“发送”按钮时，SMTP会把你的邮件从你的电脑传送到收件人的邮箱服务器。SMTP 的工作原理SMTP的工作可以分为几个步骤：连接服务器：你的邮件客户端（如Outlook、Gmail）首先连接到SMTP服务器。发送邮件：邮件客户端将邮件内容和收件人地址发送给SMTP服务器。传输邮件：SMTP服务器会根据收件人的邮箱地址找出对应的邮件服务器，并将邮件传输过去。幽默小故事：想象SMTP是一个勤劳的快递员小明，他每天都会收到很多包裹（邮件），根据包裹上的地址，他将这些包裹发送到不同的城市（邮件服务器）。如果目标城市不明确，他还会通过询问其他快递员（中继服务器）来确认路径。POP3 - 邮件的领取员POP3（Post Office Protocol 3），邮局协议第三版，是专门用于接收邮件的协议。它就像是电子邮件世界里的领取员，当你打开邮件客户端时，它会把邮件从服务器领取下来，妥妥地交到你的手上。POP3 的工作原理POP3的工作过程可以概括为：连接邮件服务器：你的邮件客户端连接到POP3服务器。认证登录：客户端使用用户名和密码进行身份验证。下载邮件：通过验证后，POP3服务器将邮件传送到客户端，邮件通常会从服务器上删除。幽默小故事：POP3就像是邮局的领取员小红，每天你去邮局（邮件客户端）的时候，她都会把当天的信件（邮件）交给你，并把邮局里的信件清空（从服务器删除）。IMAP - 邮件的同步员IMAP（Internet Message Access Protocol），互联网消息访问协议，是另一种用于接收邮件的协议，但它比POP3更为强大。IMAP就像是一位专业的同步员，确保你的邮件在多个设备上都能保持一致。IMAP 的工作原理IMAP主要有以下特点：服务器存储：邮件保存在服务器上，客户端只是访问它们。同步功能：无论你在哪个设备上查看邮件，所有操作（如标记已读、删除邮件）都会同步到服务器。部分下载：可以只下载邮件的标题或部分内容，节省带宽。幽默小故事：IMAP就像是一个高效的同步员小刘，他能把你的所有信件妥善保管在一个大仓库（服务器）里。无论你在家里、办公室还是路上查看信件，所有的更改都会同步到仓库，随时随地保持一致。第五章：FTP - 文件传输的老法师在互联网的早期，文件传输协议（FTP）几乎是网络上所有文件传输的主力军。如果说TCP/IP是互联网数据传输的交通规则，那么FTP就是其中一条重要的“高速公路”，让各种格式的文件可以在计算机之间自由穿行。那么，FTP究竟是怎样工作的？它的优缺点是什么？在现代互联网中，FTP还扮演着什么样的角色？让我们一一揭晓。FTP的基本原理FTP，全称为File Transfer Protocol，是一种用于在网络上进行文件传输的协议。它工作在TCP/IP协议的应用层，通过两个独立的通道进行文件传输：控制通道和数据通道。控制通道用来发送命令和接收响应，而数据通道则负责实际的数据传输。想象一下，你需要从朋友那里借一本书，你首先要通过电话（控制通道）告诉朋友你想借哪本书，然后朋友会告诉你书的位置和取书的时间。接着，你亲自去朋友家（数据通道）取回这本书。FTP的工作原理与此类似，它通过控制通道发送文件操作的命令，通过数据通道传输实际的文件数据。FTP的优点简单易用：FTP协议设计简单，使用方便。用户只需要一个FTP客户端软件，就可以快速连接到FTP服务器，进行文件上传和下载操作。广泛支持：FTP是互联网最早的文件传输协议之一，因此几乎所有的操作系统和网络设备都支持FTP。大文件传输：FTP特别适合传输大文件，特别是在早期互联网带宽有限的情况下，FTP的效率和可靠性得到了广泛的认可。FTP的缺点安全性不足：FTP最显著的缺点是缺乏安全性。它以明文形式传输用户的用户名和密码，以及文件内容，这意味着任何有能力截获数据的第三方都可以轻松地获取敏感信息。复杂的防火墙设置：由于FTP使用两个不同的端口进行控制和数据传输，这导致在防火墙设置上比较复杂，尤其是在现代网络环境中。缺乏现代化功能：随着互联网技术的发展，FTP逐渐显得过时，缺乏现代化的功能，如加密传输、压缩传输等。FTP在现代互联网中的应用尽管FTP在现代互联网中的地位不如从前，但它仍然在一些特定场景中发挥作用。例如：网站托管：许多网站托管服务仍然提供FTP访问，用于网站文件的上传和下载。数据备份：一些企业仍然使用FTP进行数据备份，因为它简单且被广泛支持。文件共享：在一些内部网络中，FTP仍被用来进行文件共享，特别是在需要传输大文件的场景下。历史故事FTP的历史可以追溯到1971年，当时还没有今天的互联网，而是一个叫做ARPANET的网络。FTP最初是为了解决不同计算机之间文件传输的问题而设计的。随着ARPANET的发展，FTP成为了标准协议，并逐渐改进，形成了今天我们所熟知的FTP协议。在互联网的发展早期，FTP是最主要的文件传输工具之一。许多开源软件和资料都是通过FTP服务器进行发布和共享的。那时，FTP服务器就像是一个社区图书馆，人们可以在上面找到各种有用的资源。第六章：WebSocket - 实时通信的新宠在以前，网站和服务器的通信就像是发短信：每次你需要信息时，就发送一条请求，然后等待服务器的回复。这种方式虽然可靠，但在需要频繁更新信息的场景中，就显得有些笨拙了。比如在线游戏、实时聊天应用程序等场景，这种传统的通信方式就会显得非常不及时。于是，WebSocket应运而生，如同打开了一条专用的“热线”，让客户端和服务器可以保持长连接，实时交流。WebSocket的工作原理要理解WebSocket，我们先从HTTP说起。HTTP协议是无状态的，每次请求和响应都是独立的，就像你每次发送短信都要重新拨号。而WebSocket则不同，它允许客户端和服务器之间建立一个持续的连接，可以在这个连接上进行双向通信。WebSocket的工作过程可以分为以下几个步骤：握手阶段：客户端通过HTTP请求向服务器发送一个WebSocket握手请求。这是一个升级请求，表示希望将这个HTTP连接升级为WebSocket连接。服务器如果同意，就会返回一个101状态码，表明协议切换成功。数据传输阶段：一旦WebSocket连接建立，客户端和服务器就可以通过这个连接进行双向通信。这种通信是全双工的，也就是说，客户端和服务器可以同时发送和接收数据，而不必等待对方的响应。关闭连接：当通信结束时，客户端或服务器可以发起关闭请求，WebSocket连接就会像电话一样挂断。WebSocket与HTTP的区别虽然WebSocket和HTTP都是基于TCP协议的，但它们之间有几个关键的区别：连接方式：HTTP每次请求都会建立新的连接，而WebSocket连接一旦建立，就会保持打开，直到被明确关闭。双向通信：HTTP通信是单向的，客户端发起请求，服务器响应。而WebSocket是双向通信，可以让客户端和服务器互相发送数据。数据格式：HTTP主要传输文本数据（如HTML、JSON等），而WebSocket可以传输任意格式的数据包，比如二进制数据。有趣的比喻和实例为了更加生动地理解WebSocket，我们可以把它比作一个“酒吧服务员”和“顾客”的关系。HTTP方式：顾客每次想要点饮料，都要喊“服务员”，然后服务员过来记录订单，再去吧台取饮料，最后送到顾客桌上。这个过程每次都要重复。WebSocket方式：顾客一进门，服务员就为他打开了一条专用的服务通道。顾客只需按一下桌上的按钮，服务员就会立即响应，双方可以随时交流，随时点饮料，服务效率大大提升。实例一：在线游戏在在线游戏中，游戏服务器需要实时向玩家发送游戏状态更新，比如玩家的位置、分数变化等。如果使用传统的HTTP方式，每次更新都要发送请求，延迟会非常明显。而使用WebSocket，服务器可以实时推送更新给每个玩家，体验更加流畅。实例二：实时聊天想象你在使用微信或WhatsApp这样的聊天应用。如果每次发送和接收消息都需要重新连接服务器，聊天体验会非常糟糕。WebSocket允许聊天应用保持持续的连接，消息可以即时送达，聊天变得更加实时和顺畅。第七章：结束语 - 协议的未来与展望在这一章的开头，让我们回顾一下我们已经探讨过的内容。从最基础的TCP/IP协议，到确保我们上网安全的HTTPS协议，再到帮助我们找到目标网站的DNS系统，每一种协议都在默默地为互联网的顺畅运行贡献着力量。通过这些协议，我们得以在全球范围内无缝交流，分享信息，进行各种各样的活动。协议的进化之路互联网技术在过去几十年里取得了飞速的发展，网络协议自然也不例外。从早期的简单传输协议到如今复杂而高效的通信机制，这些协议不断进化以应对新的挑战和需求。例如，随着网络安全问题日益突出，HTTPS协议的广泛应用使得我们的私人数据得到了更好的保护。而WebSocket协议的出现，则为实时通信应用提供了必要的支持。未来可能出现的新协议展望未来，随着5G网络、物联网（IoT）、人工智能（AI）等新兴技术的发展，网络协议也将继续进化，以满足新的技术需求和用户期望。1. 5G和Beyond 5G5G网络的特点是高带宽、低延迟和大规模连接能力，这对现有的网络协议提出了更高的要求。例如，超低延迟通信协议（Ultra-Reliable Low-Latency Communications, URLCC）正在成为新的研究热点，以便在极端低延迟的环境下实现可靠的数据传输。2. 物联网协议物联网设备的爆炸式增长需要高效且低功耗的通信协议。MQTT（Message Queuing Telemetry Transport）和CoAP（Constrained Application Protocol）就是为了满足这一需求而设计的。未来，我们可能会看到更多为特定应用场景优化的新协议。3. 区块链相关协议区块链技术的发展也催生了一些新的网络协议，例如用于分布式账本技术的共识协议（如PoW、PoS等）。这些协议确保了区块链网络的安全性和一致性，将在未来的去中心化应用中发挥重要作用。4. 增强隐私和安全性的协议随着用户对隐私和数据安全的需求不断增加，新的加密协议和隐私保护技术将继续发展。例如，量子密钥分发（QKD）被认为是未来抵御量子计算威胁的重要技术之一。总结通过这一系列对于网络协议的趣谈，我们不仅了解了互联网的基本工作原理，还看到了这些协议在我们的日常生活中扮演的重要角色。从基础的TCP/IP，到确保我们网络安全的HTTPS，再到支持实时通信的WebSocket，这些协议无不在为我们的互联网体验保驾护航。未来，随着技术的不断进步，网络协议必将继续演变，适应新的需求和挑战。作为互联网用户和技术爱好者，我们也将持续关注这些变化，享受更加便捷、安全和高效的网络生活。感谢你一路以来的阅读和陪伴，希望这次关于网络协议的趣谈之旅，让你对互联网背后的技术有了更深入的了解。未来的互联网世界精彩纷呈，让我们拭目以待吧！
前言个人认为，这是一道非常经典的 LeetCode Hard 题目，第一次碰到时，感觉觉得如此之精妙，想破脑袋也不会想到使用什么样合适的数据结构去解决这道问题。时隔快5年，在2024年农历新年最后一个工作日，重温下这道经典的题目。题目描述中位数是有序序列最中间的那个数。如果序列的长度是偶数，则没有最中间的数；此时中位数是最中间的两个数的平均数。例如：[2,3,4]，中位数是 3[2,3]，中位数是 (2 + 3) / 2 = 2.5给你一个数组 nums，有一个长度为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数，每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数，并输出由它们组成的数组。示例：给出 nums = [1,3,-1,-3,5,3,6,7]，以及 k = 3。窗口位置 中位数--------------- -----[1 3 -1] -3 5 3 6 7 1 1 [3 -1 -3] 5 3 6 7 -1 1 3 [-1 -3 5] 3 6 7 -1 1 3 -1 [-3 5 3] 6 7 3 1 3 -1 -3 [5 3 6] 7 5 1 3 -1 -3 5 [3 6 7] 6 因此，返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]。提示：你可以假设 k 始终有效，即：k 始终小于等于输入的非空数组的元素个数。与真实值误差在 10 ^ -5 以内的答案将被视作正确答案。原题快速门说句题外话，以现在卷的程度，是不是很多校招生必会解此题呢，想当年，这样的题目，是不太可能出现在现场笔试题目中的。^.^解题思路首先，拿到一道题目后，要分析输入规模，那其实老的题目这点是不太规范的，往往没有给你输入规模的描述，也就少了一些提示。我理解这道题目的输入规模应该如下：n = len(nums)1 <= k <= n <= 10^5那么如果是这样的数据规模，所有O(n^2)的算法可以直接劝退，不用费脑子和精力去想了。接下来，要仔细想下题目的考点是哪一个，或者哪几个组合。显然这道题目非常贴心的告诉你一个考点，滑动窗口，什么是滑动窗口，题目的示例解释的非常清楚了，k表示滑动窗口的长度，窗口往右滑动时，前面的数出窗口，后面的数进入窗口，就是这么个过程。其次，要求窗口内数据的中位数，如果每次暴力的重新计算，那么需要排序，取中间值，显然这个方案的复杂度，最坏情况下会变成O(n^2logn)，应该是过不了testcase，那么这里就要引入数据结构，以O(1)时间计算中位数，如果滑动窗口内的数字保存在一个有序数组，那么计算中位数的复杂度为O(1)就很轻松了，但是删除和添加一个元素到有序数组，操作是O(N)的，那什么数据结构删除和添加一个元素是O(logn)以下的复杂度呢？啊，想到了，类似堆或者红黑树的数据结构。那怎样使用有序的这些数据结构，实现滑动窗口中位数呢？最巧妙的设计在于，使用两个堆或者有序树的实例，一个维护前半段，一个维护后半段，那么中位数和前半段的最大值和后半段的最小值相关。如果是堆的话，前面半段使用最大堆，后面半段使用最小堆。如下图所示：这里还有一个细节点，如果k是偶数，那么前半段和后半段的数字数量相等，否则前半段多维护一个数，所以前半段维护数字的数量是 k+1 除以 2 下取整，后半段维护数字的数量是 k 除以 2 下取整。所以当 k 为奇数时，前半段堆最大值就是中位数，否则，需要将两个堆顶的数字取平均。代码实现通过上面的分析，思路有了，我实际实现时，使用了Python3，以及SortedList这个有序List（底层实现采用了二分查找和分块算法。在插入元素时，它会将列表分块，并在每个块内部使用二分查找来维护有序性。这样可以在保证列表有序的同时，提高插入和查找元素的效率），效果和堆是类似的，就是优化插入和删除元素的时间复杂度。代码如下：from sortedcontainers import SortedList class Solution: def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]: n = len(nums) l = (k+1) // 2 r = k // 2 L = SortedList(nums[:k]) R = SortedList() def L2R(): x = L.pop() R.add(x) def R2L(): x = R.pop(0) L.add(x) while len(L) > l: L2R() ans = [] if l > r: ans.append(L[-1]) else: ans.append((L[-1] + R[0]) / 2) for i in range(k, n): out_val = nums[i-k] if out_val in L: L.remove(out_val) else: R.remove(out_val) in_val = nums[i] if not L or in_val < L[-1]: L.add(in_val) else: R.add(in_val) if len(L) == l-1: R2L() elif len(L) == l+1: L2R() if l > r: ans.append(L[-1]) else: ans.append((L[-1] + R[0]) / 2) return ans 结论上述实现的代码，时间复杂度O(nlogk)，空间复杂度O(k)。所谓温故知新，最近也有些堆顶堆的题目出现在周赛上面，只是比起以前的题目，现在的题目不会直接把题目考点赤裸裸写在标题里面，需要你自己抽象转换，这步其实非常难，需要你特别熟悉这些题目考点，才能在关键时刻想到如何转换。
引言强化学习是蛋糕顶上缀着的一颗樱桃，这不是我说的，而是深度学习领域Yann LeCun教授在CoRL 2017大会上做的演讲时提及的。他著名的“蛋糕”理论。“真正的”强化学习好比蛋糕上的樱桃，监督学习好比蛋糕上的糖衣，而蛋糕本身是非监督学习（预测学习）。这里LeCun也表示，这一比喻对做强化学习的兄弟可能不太友好。认识下“樱桃”什么是强化学习呢？举个通俗的例子，强化学习就好比一个女猎手（为啥是女猎手，因为我下面用了阿塔兰忒的图像，哈哈）进入了一个迷雾森林，里面充满了各种挑战和奖励。她得自己摸索，判断哪个方向更有“糖果”，哪个方向有“陷阱”。在这个过程中，她会慢慢学会，哦，原来这样做能得到糖果，那样做会掉进陷阱。不过，这个魔法森林可不是那么容易走的。有时候，她会走错路，掉进陷阱，或者绕着同一个地方转圈圈。这时候，就需要神出手啦，给她点提示，让她知道哪个方向可能更有糖果。这就是我们常说的“智能体（agent）”和“环境（evironment）”之间的互动。通过这样的摸索和学习，阿塔兰忒最终会走出迷雾森林，这就是强化学习的魅力所在啦！小姐姐好看吧，也是我用AIGC生成的，嘿嘿。其实上面简单通俗的语言，就已经把强化学习的原理展露出来了，强化学习（Reinforcement Learning，RL）是一种通过试错来学习的方法。在深度学习中，强化学习用于训练智能体（agent）在环境（evironment）中采取行动，以最大化累积奖励。与监督学习不同，强化学习没有明确的输入输出对，智能体需要通过与环境的交互来学习最佳策略。深度学习中的强化学习原理涉及到状态（state）、动作（action）、奖励（reward）等核心概念。状态是指环境的当前状态，动作是智能体可以执行的操作，奖励是智能体采取某个动作后获得的正面或负面反馈。智能体在环境中不断尝试不同的动作，根据奖励信号来调整策略，以期获得最大的累积奖励。深度强化学习在许多应用领域取得了显著的成果，例如自动驾驶、游戏AI、机器人控制等。在实际应用中，智能体需要与环境进行大量的交互来学习最佳策略，这通常需要大量的计算资源和时间。实践环节当然只知道上面的原理是不够的，实践出真知，这块怎么去实验呢？我想起了2017年，Deepmind公司发表在nature科学杂志上的AlphaGoZero论文，当时也是通过在围棋比赛上战胜人类冠军而出圈，零人类经验，其自我训练的时间仅为3天，自我对弈的棋局数量为490万盘。但它以100:0的战绩击败前辈。围棋显然我没法找到足够算力支持，但是找个简单的游戏，还是可行的，那么我想到了一个简单好玩的小游戏《flappy bird》，我首先做了一个通过玩家控制的版本：是不是很熟悉，画面不够精致，但是也够用了，小白球作为小鸟，需要往前飞，又不能碰到白色障碍物。我自己来控制，还是比较难掌控的，很快就会死了。那么怎么实现AI控制小鸟，就是我要完成的事情。首先上面说的环境（evironment）在这个游戏中就是这个游戏本身，智能体（agent）就是这个小鸟的大脑，控制小鸟的行动。关于环境（evironment），如何抽象状态（state）是关键，我想到有好几种方法：整个游戏画面作为state，画面的每个像素点就是state，智能体（agent）需要自己理解这个画面意味着什么，障碍物的距离，小鸟的位置等等；计算某一帧时环境变量，最近的障碍物顶部占据画面高度的比例，小鸟离最近障碍物的距离占画面宽度的比例，小鸟的垂直方向的位置，小鸟的速度等等；显然不管环境（evironment）如何抽象，智能体（agent）都应该可以学会如何应对某一个具体的state，我在这篇文章中，先尝试了2这个方法，定义了5个变量：environment[0] = closest.x / (WIN_WIDTH - bird.x) # 最近的障碍物的横坐标 = closest.x，小鸟的横坐标 = bird.x，屏幕的宽度 = WIN_WIDTH environment[1] = closest.top / WIN_HEIGHT # 最近的障碍物上半部分下沿 = closest.top，屏幕的高度 = WIN_HEIGHT environment[2] = closest.bottom / WIN_HEIGHT # 最近的障碍物下半部分上沿 = closest.bottom，屏幕的高度 = WIN_HEIGHT environment[3] = bird.y / WIN_HEIGHT # 小鸟的纵坐标 = bird.y，屏幕的高度 = WIN_HEIGHT environment[4] = bird.velocity / 10 # 小鸟的往前的速度 = bird.velocity 智能体（agent）简单定义为一个深度神经网络（4层的全连接网络），输入就是上面的evironment数组变量，输出为action，在这个游戏中就是两种行为：1、向上飞；2、保持不动，所以神经网络最终输出为2。brain = Sequential( Linear(5, 16, name='linear1'), ReLU(name='relu1'), Linear(16, 64, name='linear2'), ReLU(name='relu2'), Linear(64, 16, name='linear2'), ReLU(name='relu3'), Linear(16, 2, name='linear2'), ) 那么就来到了最难设计的部分，奖励（reward）如何设计，这里又必须展开一些强化学习训练细节来讲下。强化学习的训练数据，是通过智能体（agent）和环境（evironment）不停的交互，产生的一系列动作（action）概率和一系列奖励（reward）来进行训练的。所以奖励（reward）的设计尤为关键，简单讲就是在迷雾森林中，你要准确的告诉阿塔兰忒往左是陷阱，会被扣分，往右是糖果，是个奖励，会加分。所以在每个具体游戏中，都需要去明确的制定合理的奖励（reward）规则，最后智能体（agent）才会做出利益最大化的动作（action），那么我这里是这样定义的：for pipe in self.pipes: if pipe.hits(bird): reward = -450 # 撞到障碍物 pipe，扣450分 done = True break elif pipe.avoid(bird): reward = 60 # 躲避障碍物 pipe，得60分 if bird.hitBottom(): reward = -450 # 小鸟撞到底部，扣450分 elif bird.hitTop(): reward = -450 # 小鸟撞到底部，扣450分 别小看这几句代码，简单但是扣分和得分是会影响智能体（agent）学习的速度的。我这里是调试了不少时间，才完成了整个训练，如果有朋友有兴趣，我可以详细来讲下这部分的训练实现和调试过程，这篇文章里我不进行展开。结论展示下最终的效果：感觉效果还是十分nice的，视频转gif没处理好，感觉gif图的质量一般，我自己看AI玩的效果是真的6，我想下次可以再继续使用图片作为环境（environment），改一下模型结构（可能需要换成resnet等cnn作为backbone），硬train一发，也能得到类似的效果，这个就留作后续的任务，挖个小坑。
导语好久没更新，从去年十月份来，工作突然很忙，平时剩下的时间用来学习、健身，给到总结写作，没剩下时间。加上人每隔一段时间，都会产生懈怠，就停更个人技术博客近半年。想想去年因为疫情在家办公期间，某种程度上来看，还是很幸福的，上班不用来回坐车，节省下来的时间，可以花在自己感兴趣的事情上，产生一些输出。OK，言归正传，今天闲来无事，我翻出了女儿去年玩过的小游戏，她妈妈给她买的这类智力小游戏有不少，很偶尔会瞧见她们娘俩一起玩，估计是不太感兴趣。今天这款游戏，我小时候没见过，是一个新的小游戏，我就起一个应景的名字，《兔兔大战小狐狸》，今年是兔年，兔子要逃脱小狐狸的围追堵截，逃到安全的小土坑里，躲起来。游戏介绍游戏棋盘是一个5*5的方盘，左上、右上、左下、右下以及正中间有5个小土坑，如果棋盘上所有的兔子，都在土坑待着，就完成了任务。具体规则，我不码字，看如下说明：如上面所说，游戏内置了60个关卡（因为随便摆放，是不一定能完成任务的），也有最少步骤的解答，我自己尝试玩了几个简单关卡（最少10步以内可以完成的），貌似还挺简单的。然后，我翻到了关卡的最后4个关卡，哇，一下子震惊了，好难，最后一关官方解答，需要87步完成任务。一些疑问小朋友如果自己完成前面简单、中等题目，已经很不容易了。至于困难、专家级别的题目，如果能完成，小脑瓜不是一般聪明，非常了不起。但我想了下，官方说最后一题87步是最少步骤，这个怎么证明官方没错呢？或者有更少步骤的解法？我心想，是不是写个算法求解，看看是否是对的。求解过程首先是需要有个建模过程，棋盘规格是5*5方盘，上面有25个位置，5个位置是土坑。可以放置的角色是小狐狸、兔兔和蘑菇。兔兔和蘑菇都只占用一个格子，狐狸占用两个格子，还有头和尾巴，但我仔细想了下，狐狸头和尾巴，其实在算法中不需要区分，只需要区分狐狸是竖着还是横着放置的，你可以理解为狐狸的头总是在左边或者上方，尾巴在右边或者下方。这样对算法实现更简单。比如60题，我会使用5个字符串表示5行，"...X." "FF.R." "..X.R" "X...." "...R." 字符X表示蘑菇，F表示小狐狸（头或者尾），R表示小兔兔，最后.表示空位。算法显然就是搜索算法，DFS还是BFS呢？我先用DFS实现了半天，会TLE，根本算不出最少步骤的解法。我后来想了下，最少步骤的解法确实不太适合DFS，因为DFS可能需要搜索完所有的解法空间（也可能我剪枝还不够），所以无法算出最少步骤。在DFS上，我其实花了不少时间优化，这个题目比刷题难，因为是自己出给自己的，也没有什么参考答案，都只能自己去想，如何建模和实现搜索算法，总之，自己试了半天，也不work。OK，那么我就来试下BFS搜索，大概的逻辑步骤如下：1. 建立一个队列，将游戏初始棋盘局面放到队列中； 2. 一一取出当前局面，评估可能变为的下一个局面； 3. 评估当前局面可能可以行动的小兔兔、小狐狸坐标； 4. 一一取出小兔兔、小狐狸，评估她们可能的移动到的位置； 5. 通过改变小兔兔、小狐狸位置，得到下一个局面； 6. 将2～5变化的到的局面插入队列（下一轮）； 7. 遍历完上一轮队列，步骤+1； 8. 把下一轮队列变为需要遍历的当前队列，重复2～7步骤； 代码实现：int smartGame(vector<string> board) { int n = board.size(); // 判断游戏是否结束 set<vector<int>> finish_pos = {{0, 0}, {0, n-1}, {n-1, 0}, {n-1, n-1}, {n/2, n/2}}; function<bool(vector<vector<int>>)> isFinished = [&](vector<vector<int>> ani_pos) -> bool { int n = ani_pos.size(); for (int i = 0; i < n; ++i) { if (ani_pos[i][2] != -1) continue; vector<int> pos = {ani_pos[i][0], ani_pos[i][1]}; if (finish_pos.find(pos) == finish_pos.end()) return false; } return true; }; // 分析棋盘局面，哪些可能移动的小兔兔、小狐狸所在位置 function<void(vector<string>, vector<vector<int>>&)> analyzeBoard = [&](vector<string> board, vector<vector<int>>& ani_pos) { for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (board[i][j] == 'X') { continue; } else if (board[i][j] == '.') { continue; } else if (board[i][j] == 'F') { if ((i == 0 || board[i - 1][j] != 'F') && (i < n - 1 && board[i + 1][j] == 'F')) { // vertical fox ani_pos.push_back({i, j, 1}); } else if ((j == 0 || board[i][j - 1] != 'F') && (j < n - 1 && board[i][j + 1] == 'F')) { // horizontal fox ani_pos.push_back({i, j, 2}); } } else if (board[i][j] == 'R') { ani_pos.push_back({i, j, -1}); } } } }; // 棋盘局面上某个小兔兔、小狐狸，做出某个移动后，返回新的局面 function<vector<string>(vector<string>, vector<int>, vector<int>)> applyBoard = [&] (vector<string> board, vector<int> pos, vector<int> move) -> vector<string> { if (pos[2] == -1) { board[pos[0]][pos[1]] = '.'; board[pos[0]+move[0]][pos[1]+move[1]] = 'R'; } else { if (pos[2] == 1) { board[pos[0]][pos[1]] = '.'; board[pos[0]+1][pos[1]] = '.'; board[pos[0]+move[0]][pos[1]+move[1]] = 'F'; board[pos[0]+move[0]+1][pos[1]+move[1]] = 'F'; } else if (pos[2] == 2) { board[pos[0]][pos[1]] = '.'; board[pos[0]][pos[1]+1] = '.'; board[pos[0]+move[0]][pos[1]+move[1]] = 'F'; board[pos[0]+move[0]][pos[1]+move[1]+1] = 'F'; } } return board; }; // 分析棋盘局面，可能移动的小兔兔、小狐狸，能做出哪些移动 function<vector<vector<int>>(vector<string>, vector<int>)> nextMoves = [&] (vector<string> board, vector<int> pos) -> vector<vector<int>> { vector<vector<int>> moves; if (pos[2] == -1) { // up vector<int> t = pos; while (t[0] >= 0 && board[t[0]][t[1]] != '.') t[0] -= 1; if (t[0] >= 0 && board[t[0]][t[1]] == '.' && t[0] < pos[0]-1) { moves.push_back({t[0]-pos[0], 0}); } // down t = pos; while (t[0] < n && board[t[0]][t[1]] != '.') t[0] += 1; if (t[0] < n && board[t[0]][t[1]] == '.' && t[0] > pos[0]+1) { moves.push_back({t[0]-pos[0],0}); } // left t = pos; while (t[1] >= 0 && board[t[0]][t[1]] != '.') t[1] -= 1; if (t[1] >= 0 && board[t[0]][t[1]] == '.' && t[1] < pos[1]-1) { moves.push_back({0, t[1]-pos[1]}); } // right t = pos; while (t[1] < n && board[t[0]][t[1]] != '.') t[1] += 1; if (t[1] < n && board[t[0]][t[1]] == '.' && t[1] > pos[1]+1) { moves.push_back({0, t[1]-pos[1]}); } } else if (pos.size() == 3) { if (pos[2] == 1) { // up if (pos[0] > 0 && board[pos[0]-1][pos[1]] == '.') { moves.push_back({-1, 0}); } // down if (pos[0] < n-2 && board[pos[0]+2][pos[1]] == '.') { moves.push_back({1, 0}); } } else if (pos[2] == 2) { // left if (pos[1] > 0 && board[pos[0]][pos[1]-1] == '.') { moves.push_back({0, -1}); } // right if (pos[1] < n-2 && board[pos[0]][pos[1]+2] == '.') { moves.push_back({0, 1}); } } } return moves; }; // 棋盘局面的签名，用作去重 function<string(vector<string>)> signature = [&](vector<string> board) -> string { string r; for (auto l : board) { r += l; } return r; }; // 1. 初始棋盘局面，入到队列 queue<vector<string>> q; unordered_set<string> seen; q.push(board); seen.insert(signature(board)); int ans = 0; while (!q.empty()) { int size = q.size(); while (size --) { // 2. 一一取出当前局面，评估可能变为的下一个局面； vector<string> t = q.front(); q.pop(); vector<vector<int>> ani_pos; // 3. 评估当前局面可能可以行动的小兔兔、小狐狸坐标； analyzeBoard(t, ani_pos); if (isFinished(ani_pos)) { return ans; } // 4. 一一取出小兔兔、小狐狸，评估她们可能的移动到的位置； for (auto pos: ani_pos) { vector<vector<int>> moves = nextMoves(t, pos); for (int i = 0; i < moves.size(); ++i) { vector<int> move = moves[i]; // 5. 通过改变小兔兔、小狐狸位置，得到下一个局面； vector<string> nxtBoard = applyBoard(t, pos, move); string sig = signature(nxtBoard); if (seen.find(sig) == seen.end()) { // 6. 将2～5变化的到的局面插入队列（下一轮）； q.push(nxtBoard); seen.insert(sig); } } } } // 7. 遍历完上一轮队列，步骤+1； // 8. 把下一轮队列变为需要遍历的当前队列，重复2～7步骤； ans++; } return -1; } 还要强调一点优化，我用了一个unordered_set去做棋盘局面去重，曾经看见过的局面，就不需要再去考虑了，因为即使从该局面出发有解，肯定也不会比之前出现时更优的，这个很好理解吧。结论最终通过算法验证，最后4题的最优解确实和官方的步骤数量一样，并且我后来记录了最优解算法的到的步骤，虽然有些顺序和官方题解不一致，我仔细比对了下，仅仅是某些步骤中，可以交换顺序而已，不是计算错误。别看是思路很简单，就是BFS搜索算法，但是面对一个没有参考答案的问题，建模、优化和调试，哪一样都不轻松，很开心，今天可以完成任务，并且写下这篇文章，明天继续加油～
导语最近在学python数据处理的几个常用库，分别是numpy、matplotlib和pandas。这几个库应该是python数据处理的第三方神器库，学了一招图像手绘风格处理，非常有意思，记录下来。图像手绘风格手绘风格示例：第二幅就是有手绘的感觉，当然是基于第一章图片处理得到的。手绘风格可以总结为以下几点：黑白灰色边界线条较重相邻的像素，如果是相同或者相近色彩趋于白色略有光源效果具体如何通过程序实现呢？接下来就来逐一介绍下。梯度的重构利用像素之间的梯度值和虚拟深度值对图像进行重构，根据灰度变换来模拟人类视觉的明暗程度。代码如下：import numpy as np from PIL import Image a = np.asarray(Image.open('xxx.jpeg').convert('L')).astype('float') depth = 10. grad = np.gradient(a) grad_x, grad_y = grad grad_x = grad_x*depth/100. grad_y = grad_y*depth/100. 下面这行代码是通过pillow库读取原先图像，RGB值转为灰度值。a = np.asarray(Image.open('xxx.jpeg').convert('L')).astype('float') 这几行代码是计算灰度图像的像素间梯度，并且根据虚拟深度重构梯度值，depth初始化为10，depth取值0～100。depth = 10. grad = np.gradient(a) grad_x, grad_y = grad grad_x = grad_x*depth/100. grad_y = grad_y*depth/100. 光源效果根据灰度变化来模拟人类视觉的远近程度。设计一个位于图像斜上方的虚拟光源光源相对于图像的俯视角为Elevation，方位角为Azimuth建立光源对各个点梯度值的影响函数运算各个点的新像素值代码如下：vec_el = np.pi/2.2 # 光源的俯视角度，弧度值 vec_az = np.pi/4. # 光源的方位角度，弧度值 dx = np.cos(vec_el)*np.cos(vec_az) # 光源对x轴的影响 dy = np.cos(vec_el)*np.sin(vec_az) # 光源对y轴的影响 dz = np.sin(vec_el) # 光源对z轴的影响 A = np.sqrt(grad_x**2 + grad_y**2 + 1.) # 光源归一化 uni_x = grad_x/A uni_y = grad_y/A uni_z = 1./A b = 255*(dx*uni_x+dy*uni_y+dz*uni_z) 最后，对像素值进行裁剪至0～255，防止越界。b = b.clip(0, 255) im = Image.fromarray(b.astype('uint8')) im.save('xxxHD.jpeg') 最后程序虽然小，效果还是比较有意思的，我给女儿看了，她说你画画不是很丑的吗，为啥画的这么好？哈哈，我和她说，这是计算机处理的，不是我手画的，小小的年纪充满大大的问号。其实，这里最重要的是对图像灰度值计算梯度，这是Computer Vision里面传统图像处理的关键，CV相关的技术，由于可以可视化，实现之后，还是比较有意思的。
导语作为80后，小时候一定玩过一款经典游戏，俄罗斯方块，英文名称Tetris。玩的画面在脑海中已是“上古”时期的事情，但也并不陌生，因为刷视频时，偶尔还能看到顶尖世界高手对决，他们玩的俄罗斯方块难度极大，游戏模式是对决，比谁的分数高，有些比赛到最后不仅自己消砖块得分，而且还可以通过得分不停给对手制造麻烦。看到鹅厂组织的一次极客挑战赛，主题就是俄罗斯方块游戏，当然游戏名称叫“鹅”罗斯方块，蛮鹅厂风格的，可可爱爱。我比较感兴趣，当时也报名参加了，最后是在2000+入场选手排名在9位，比赛结束后，我又花了点时间调试了自己的算法参数，最终比赛分数可以刷进TOP3。谨以此文，记录自己的所思所想。如何活着第一步想到的是，如何“活着”，就和当前经济形势下，各行各业都得考虑先“活着”，能不能得到高分其次，先得“不死”。我自己写了一个俄罗斯方块的游戏界面，和传统俄罗斯方块游戏一样，如何拼搭这些小图形，让高度不超过游戏规定的高度，满行就会消除，消除的行数有4种情况1行、2行、3行和4行。针对如何活着的俄罗斯方块AI算法比较多了，全网比较有名的是Pierre Dellacherie算法。我这里搜索到一篇El-Tetris – An Improvement on Pierre Dellacherie’s Algorithm，大家可以参考下。首先，分析下俄罗斯方块中方块的种类，一共有7种图形：上面图中有7种不同的形状，我在代码里面的表示为坐标法，以(0, 0)为原点，标示周围那几个点对应图形需要占用。BrickShapes = [ # I [[[-2, 0], [-1, 0], [0, 0], [1, 0]], [[0, -1], [0, 0], [0, 1], [0, 2]]], # L [[[-2, 0], [-1, 0], [0, 0], [0, 1]], [[0, 0], [0, 1], [0, 2], [1, 0]], [[0, -1], [0, 0], [1, 0], [2, 0]], [[-1, 0], [0, -2], [0, -1], [0, 0]]], # J [[[-2, 0], [-1, 0], [0, -1], [0, 0]], [[-1, 0], [0, 0], [0, 1], [0, 2]], [[0, 0], [0, 1], [1, 0], [2, 0]], [[0, -2], [0, -1], [0, 0], [1, 0]]], # T [[[0, -1], [0, 0], [0, 1], [1, 0]], [[-1, 0], [0, -1], [0, 0], [1, 0]], [[-1, 0], [0, -1], [0, 0], [0, 1]], [[-1, 0], [0, 0], [0, 1], [1, 0]]], # O [[[-1, 0], [-1, 1], [0, 0], [0, 1]]], # S [[[-1, 0], [-1, 1], [0, -1], [0, 0]], [[-1, -1], [0, -1], [0, 0], [1, 0]]], # Z [[[-1, -1], [-1, 0], [0, 0], [0, 1]], [[-1, 0], [0, -1], [0, 0], [1, -1]]] ]比如竖棍“I”的图形，有两种rotation，所以有两组坐标的标示方式，其他图形类似，只不过rotation数量不同： # I, two rotations [[[-2, 0], [-1, 0], [0, 0], [1, 0]], [[0, -1], [0, 0], [0, 1], [0, 2]]], El-Tetris算法是传统AI人工智能，不要因为人工智能，就觉得很复杂，其实不论传统的AI算法还是目前深度学习的人工算法，一般都不会很复杂，深度学习AI算法不在此文讨论范围。基本上传统AI算法就是列举所有的可能性，通过计算预先建模的公式，评估最优秀的一个局面，做出选择，一直循环往复，直到游戏结束。我们看下El-Tetris算法的公式：f(x) = b0x0+b1x1+b2x2...bnxn其中x0,x1,x2...xn都是一些特征，b0,b1,b2...bn是一些系数（通过数学方法或者机器学习方法等手段推算出的经验值），对应特征和系数相乘相加之后，得到的就是当前局面+此选择的局面评估分数，是不是很简单，看上去是一个线性公式，但是效果出奇的好，原因还是在于每一个特征x的合理性以及系数b的合理性。系数b不需要我们实现，就是一组常数字，作者说是通过particle swarm optimization寻找到的，具体怎么找，我也不太清楚，先拿来用吧。另外要提一下，算法可以通常可以考虑当前图形+下一个图形，也可以只考虑当前图形，据说效果差不多，我这里只考虑当前图形的所有可能的操作，每一个图形出来，其实只需要考虑图形放置的position和rotation两个变量。所以整体的流程：while the game is not over: examine piece given find the best move possible (an rotation and a position) play piece repeat 上面铺垫这么多，那么一共建模了几种特征呢？一共是6种，如下：Landing Height: The height where the piece is put (= the height of the column + (the height of the piece / 2)).Rows eliminated: The number of rows eliminated.Row Transitions: The total number of row transitions. A row transition occurs when an empty cell is adjacent to a filled cell on the same row and vice versa.Column Transitions: The total number of column transitions. A column transition occurs when an empty cell is adjacent to a filled cell on the same column and vice versa.Number of Holes: A hole is an empty cell that has at least one filled cell above it in the same column.Well Sums: A well is a succession of empty cells such that their left cells and right cells are both filled.Landing Height和Rows eliminated比较好理解，其他对着下图解释下：Row Transitions总数是16，最底部一行为2，依次往上2、2、2、2、4、2，注意我这里处理为左右边界外是被占据的，所以倒数第四行靠最左边有个空，算存在一次row transition，另外空行我没算（我猜测也可以计算row transition，不影响最后结果）。Column Transitions总数是26，如果理解了Row Transitions，这个计算方法是一样的。 Number of Holes总数是4，看下上图，应该知道什么叫“洞”，被封闭了的空单元格。Well Sums总数是5，如果左右两边单元格被占，顶上又没有被封闭，可以算作一个“井”格子，第一列一个单元格，左边界外算占据，右边也被占据，所以也算作“井”格子，深度为1，第三列就一个单元格，井深度为1，第六列有2个空单元格，井深度和为1+1+1+2。这个比较难理解，多看几遍。对应的系数如下：Landing Height = -4.500158825082766Rows eliminated = 3.4181268101392694Row Transitions = -3.2178882868487753Column Transitions = -9.348695305445199Number of Holes = -7.899265427351652Well Sums = -3.3855972247263626计算代码如下：def find_best_play(current_piece, grid): if current_piece.y < 0: False, 0, 0 rotations = len(current_piece.shape) x_moves = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5] rotation = 0 x_move = 0 best_play_score = float("-inf") for i in range(rotations): for j in x_moves: candidate_piece = copy.deepcopy(current_piece) candidate_grid = copy.deepcopy(grid) candidate_piece.rotation += i candidate_piece.x += j if not (valid_space(candidate_piece, candidate_grid)): continue valid, score = evaluate(candidate_piece, candidate_grid) if valid and score > best_play_score: best_play_score = score rotation = i x_move = j return True, rotation, x_move def evaluate(current_piece, grid): while True: current_piece.y += 1 if not (valid_space(current_piece, grid)) and current_piece.y > 0: current_piece.y -= 1 break shape_pos = convert_shape_format(current_piece) for i in range(len(shape_pos)): x, y = shape_pos[i] if y > -1: grid[y][x] = current_piece.color heights_points = (20 - current_piece.y + current_piece.height[current_piece.rotation % len(current_piece.height)]) * float(-4.500158825082766) rows_removed = get_rows_removed(grid) * float(6.4181268101392694) rows_transitions = get_row_transitions(grid) * float(-3.2178882868487753) cols_transitions = get_col_transitions(grid) * float(-9.348695305445199) num_of_holes = get_num_of_holes(grid) * float(-14.899265427351652) wells = get_well_sums(grid) * float(-6.3855972247263626) return True, heights_points + rows_removed + rows_transitions + cols_transitions + num_of_holes + wells find_best_play列举所有可能的操作，evaluate就是评估局面的分数，其中几个计算特征值的函数：def get_row_transitions(grid): rows = len(grid) cols = len(grid[0]) transitions = 0 last_pos = True for i in range(rows - 1, -1, -1): if is_empty_row(grid[i]): continue for j in range(cols): if (grid[i][j] in brick.SHAPES_COLORS) != last_pos: transitions += 1 last_pos = (grid[i][j] in brick.SHAPES_COLORS) if not last_pos: transitions += 1 last_pos = True return transitions def get_col_transitions(grid): rows = len(grid) cols = len(grid[0]) transitions = 0 last_pos = True for j in range(cols): for i in range(rows - 1, -1, -1): if (grid[i][j] in brick.SHAPES_COLORS) != last_pos: transitions += 1 last_pos = (grid[i][j] in brick.SHAPES_COLORS) if not last_pos: transitions += 1 last_pos = True return transitions def get_num_of_holes(grid): rows = len(grid) cols = len(grid[0]) holes = 0 previous_row_holes = [0 for _ in range(cols)] for i in range(rows)[1:]: for j in range(cols): if grid[i][j] == (0, 0, 0) and (grid[i-1][j] != (0, 0, 0) or previous_row_holes[j] == 1): holes += 1 previous_row_holes[j] = 1 return holes def get_well_sums(grid): rows = len(grid) cols = len(grid[0]) well_sums = 0 # Check for well cells in the "inner columns" of the board. # "Inner columns" are the columns that aren't touching the edge of the board. for j in range(cols-1)[1:]: for i in range(rows - 1, -1, -1): if grid[i][j] == (0, 0, 0) and grid[i][j-1] != (0, 0, 0) and grid[i][j+1] != (0, 0, 0): well_sums += 1 k = i+1 while k < rows: if grid[k][j] == (0, 0, 0): well_sums += 1 else: break k += 1 # Check for well cells in the leftmost column of the board. for i in range(rows - 1, -1, -1): if grid[i][0] == (0, 0, 0) and grid[i][8] != (0, 0, 0): well_sums += 1 k = i + 1 while k < rows: if grid[k][0] == (0, 0, 0): well_sums += 1 else: break k += 1 # Check for well cells in the rightmost column of the board. for i in range(rows - 1, -1, -1): if grid[i][cols-1] == (0, 0, 0) and grid[i][cols-2] != (0, 0, 0): well_sums += 1 k = i + 1 while k < rows: if grid[k][cols-1] == (0, 0, 0): well_sums += 1 else: break k += 1 return well_sums 完成后的效果特别好，尽管一度高度和累积的形状不太好看：但是AI会慢慢控制住局面，变成如下局面：我尝试自己手动玩了下，鹅厂给出的图形顺序，还真的不是很好玩，稍不小心就挂了，因为图形的占比不是均分的，S型和Z型比较多，过了一会，貌似EI-Teris算法也会“顶”不住。。。仔细看了下方块图形生成的代码，每个方块图形的生成概率不等，比如长条“I”型的生成概率才2/29：def getShapeIndex(self): weight = self.rand_number % 29 shape_index = 0 if 0 <= weight <= 1: shape_index = 0 elif 1 < weight <= 4: shape_index = 1 elif 4 < weight <= 7: shape_index = 2 elif 7 < weight <= 11: shape_index = 3 elif 11 < weight <= 16: shape_index = 4 elif 16 < weight <= 22: shape_index = 5 else: shape_index = 6 return shape_index 所以正常的俄罗斯方块AI算法远远不够，调整了一下估值函数的系数（针对掉落方块形状分布不均的情况），我发现最多也只能获得20w分不到（方块图形数量仅仅10000块），离当天的第一名130w+分，差距很大。追求“财富”仔细理解下鹅罗斯方块的积分规则，“富贵险中求”：由于就仅仅有10000块方块，我考虑使用动态规划，将所有可能的局面求出，再计算能够累积得到的最大分数，当然，由于状态总数有2^200之多，保存所有的局面不现实，必须要舍去大部分的局面。一开始的想法是：在扩展的过程中，按照安全性估价函数筛查出固定数目状态进行保存（目标是局面安全），经过固定步数，再对分数进行贪心，取出唯一最高分状态继续扩展（目标是高分），这样反反复复尝试，不行，因为太早贪心，容易丢失掉“潜力”的局面，后面会得到高分局面，所以尽可能保留安全局面，所谓“留得青山在，不怕没柴烧”，直接把分数的因素引入估价函数，不再额外对做分数做贪心。evaluation = board_evaluator.Evaluate(k) - v.score * self.score_weight 其中board_evaluator.Evaluate(k)中，k就是当前局面，self.score_weight我手动尝试出来的取值为1/38，Evaluate函数就是：PARAMS = [ -1, 3.2178882868487753, 9.348695305445199, 7.899265427351652 # 3.3855972247263626 ] def Evaluate(board): return PARAMS[0] * board.getCount() + PARAMS[1] * board.getRowTransition() + \ PARAMS[2] * board.getColTransition() + PARAMS[3] * board.getNumberOfHoles() 其中Landing Height和Well sums两个特征不再评估函数内，甚至我觉得“井”的局面对取得高分是有益的（没尝试加回来，可以试试看），另外增加了当前局面下的被占据了的格子总数和当前可以得到的分数和。靠着这个估值函数，对于当前方块放置完成的局面只保留100个最好的（评估分数最高的），一直迭代到10000个，取出得分最高的局面，还原出所有的操作列表，在本地模拟AI的操作，效果特别棒，AI会自动的留一条缝，尝试将其他空间填满填好，比赛期间取得了85w的分数，赛后我手动调试后，分数涨到125w左右，效果图如下所示：总结总结的文章看上去很简单，其实过程特别煎熬，反反复复在本地尝试，实现过程也有不少小问题，调试起来也很麻烦，有怀疑、有纠结、有确定，到最后的效果符合预期，真的不容易，比如为啥score_weight要取1/38，估值函数的整体建模，为什么要减除这个特征，要加另外一个特征，怎么剪枝。整个比赛期间，我只取得了85w的分数，赛后再经过调试得到了125w的分数，就叫此算法为“富贵险中求”算法，可以使用此算法的前提是必须知道方块完整的掉落顺序。
导语上周记录了极客挑战赛的情况，其中一道非常有趣的题，本周继续，本周题目也是游戏，大概的游戏形式是横版闯关类的：还是借用网上游戏的图片说明下大概的意思，玩家一个英雄，进入一个场景（横版的），会有一个（每个场景仅一个）怪物与玩家英雄对抗，玩家有血条，有蓝量，6个技能（其中1个技能是升级其他5个技能），然后需要做的就是打到场景中的怪物，冲往下一个关卡。摸索有了之前游戏的经验之后，我知道了，上来就是一顿摸索，其实和上周说的游戏类似，客户端是个状态机，英雄有血量和蓝量，每次和怪物对抗之后，客户端会上传一堆指令到服务端，服务端重放这一堆指令，看最终的英雄状态和怪物状态是否合法，是不是英雄还活着，怪物已经死亡，然后就可以进行到下一关。一共是有9个怪物，一旦进入游戏，可以得到所有怪物的属性：bosses = [ { "Name": "小壶守卫", "Model": "Boss_ball", "Hp": 396, "Skill1_harm": 63, "Skill2_harm": 71, "Award_hp": 133, "Award_mp": 1695, "New_star": 0 }, { "Name": "苇名二心", "Model": "Boss_sword", "Hp": 532, "Skill1_harm": 59, "Skill2_harm": 89, "Award_hp": 157, "Award_mp": 1678, "New_star": 0 }, { "Name": "古达", "Model": "Boss_guda", "Hp": 643, "Skill1_harm": 57, "Skill2_harm": 97, "Award_hp": 142, "Award_mp": 1362, "New_star": 1 }, { "Name": "中壶守卫", "Model": "Boss_ball", "Hp": 668, "Skill1_harm": 59, "Skill2_harm": 67, "Award_hp": 178, "Award_mp": 1458, "New_star": 0 }, { "Name": "苇名三心", "Model": "Boss_sword", "Hp": 687, "Skill1_harm": 31, "Skill2_harm": 101, "Award_hp": 212, "Award_mp": 2659, "New_star": 0 }, { "Name": "黑古达", "Model": "Boss_guda", "Hp": 852, "Skill1_harm": 77, "Skill2_harm": 91, "Award_hp": 132, "Award_mp": 4157, "New_star": 2 }, { "Name": "大壶守卫", "Model": "Boss_ball", "Hp": 1502, "Skill1_harm": 71, "Skill2_harm": 105, "Award_hp": 154, "Award_mp": 3089, "New_star": 0 }, { "Name": "苇名零心", "Model": "Boss_sword", "Hp": 1611, "Skill1_harm": 97, "Skill2_harm": 127, "Award_hp": 189, "Award_mp": 6493, "New_star": 0 }, { "Name": "超黑古达", "Model": "Boss_guda", "Hp": 2988, "Skill1_harm": 101, "Skill2_harm": 153, "Award_hp": 0, "Award_mp": 0, "New_star": 3 } ]简单讲一下“Award_hp”和“Award_mp”这两个值，就是你击败怪物后，得到的奖励，可以分别增加对应的hp和mp，其他字段就是对应意思了。hero = { "Hp": 520, "Mp": 1806, "Power": { "Cost_mp": 233, "Attack_increase": [ { "skill_i": 1, "skill_j": 1, "skill_k": 1, "skill_l": 1, "skill_u": 1 }, { "skill_i": 6, "skill_j": 1, "skill_k": 2, "skill_l": 3, "skill_u": 5 }, { "skill_i": 6, "skill_j": 2, "skill_k": 2, "skill_l": 3, "skill_u": 4 }, { "skill_i": 6, "skill_j": 1, "skill_k": 2, "skill_l": 3, "skill_u": 4 }, { "skill_i": 6, "skill_j": 2, "skill_k": 2, "skill_l": 3, "skill_u": 4 }, { "skill_i": 6, "skill_j": 2, "skill_k": 3, "skill_l": 3, "skill_u": 5 }, { "skill_i": 7, "skill_j": 1, "skill_k": 2, "skill_l": 4, "skill_u": 5 }, { "skill_i": 7, "skill_j": 2, "skill_k": 3, "skill_l": 3, "skill_u": 5 }, { "skill_i": 7, "skill_j": 2, "skill_k": 2, "skill_l": 4, "skill_u": 5 }, { "skill_i": 7, "skill_j": 2, "skill_k": 3, "skill_l": 3, "skill_u": 5 }, { "skill_i": 7, "skill_j": 2, "skill_k": 3, "skill_l": 4, "skill_u": 5 } ] }, "Skills": { "skill_i": { "Cost_mp": 489, "Attack_hp": 187 }, "skill_j": { "Cost_mp": 134, "Attack_hp": 50 }, "skill_k": { "Cost_mp": 186, "Attack_hp": 70 }, "skill_l": { "Cost_mp": 251, "Attack_hp": 95 }, "skill_u": { "Cost_mp": 352, "Attack_hp": 134 } } } 经过几次手打的尝试之后，客户端上传的指令，类似["skill", "skill_j"]，["attack", "Hero", "Boss", "skill_j"]这两条连续指令，表示英雄发动了技能skill_j，并且命中了怪物，反过来，怪物击中英雄也有类似连续指令，不展示了。手打很容易通过前三关，后面几关越来越难，手残党直接放弃。思考一番之后，其实客户端只需要传送英雄攻击怪物的动作序列即可，不需要构造英雄“挨打”的序列，嘻嘻～所以问题转换为英雄的蓝量够不够击败怪物。如果不存在技能升级，那么就是一个背包问题，算是一个无限背包问题（实际上整体问题属于一个有限背包问题，因为技能其实是有施放次数上限，这里计算背包问题时，先不考虑）。确定了是无限背包问题，也有两条建模路径：确定的蓝量能够产生最大的耗血量；确定的血量用最少多少蓝量可以完成；OK，二选一，上来我选择了第一条，很快能写出背包问题代码：# W指的是最大的蓝量，w是技能耗的蓝量树组，v是技能产生的伤害 # 最后dp返回的数组，每一个元素有两位[x, y]，x表示产生的最大伤害，y表示达到这个最大伤害使用的技能序号 def knapsack01D(w, v, W): n = len(w) dp = [] for i in range(W+1): dp.append([0,-1]) for i in range(n): knapsackComplete(dp, w[i], v[i], i) def knapsackComplete(dp, w, v, i): W = len(dp) j = w while j < W: if dp[j-w][0]+v > dp[j][0]: dp[j][2] = i dp[j][0] = max(dp[j-w][0]+v, dp[j][0]) j += 1 虽然背包代码写得很轻松，但是后面就陷入了一个死胡同，我拿着这个函数，疯狂尝试，怎么试呢？前面1～3关我不升级技能，看能不能通过，发现不行，那升一级，发现可以通关，升两级不行，那我就升一级，然后到了4～6关，我再用此策略，结果发现弄来弄去，只能通过4～6关，获得第二颗星星的奖励，死活打不过7～9关怪物。问题建模当我要放弃的时候，我看了一眼答案提示，答案没有代码实现哈，但其中有一句话点醒了我：设状态 dpAi 表示强化i次的情况下，伤害j血量的最小蓝耗，dpBi 表示强化i次的情况下，完成j这个boss之后剩余的最大蓝耗，然后两个dp依次计算，就可以得到最终策略了。这句话得多读几遍，也就是说，我们可以把所有升级后的对应血量蓝耗情况都计算出来，然后还得多算一个矩阵，表示强化了i次情况下，击败第j个怪物之后，剩余的最大蓝耗，首先上面我算的蓝耗和血量的背包问题，反过来建模，通过确定血量耗最少蓝的结果，去计算击败某个怪物之后剩余蓝量。想清楚整体模型之后，动手写了代码，确定血量消耗最小蓝耗问题，也不是特别难，基于这个结果，去构建完成j这个boss之后剩余的最大蓝耗，稍微多想下，也可以写出来，代码大致如下：# 根据hero和boss的数组初始化背包问题相对应的值 # dp最后保存的是血量j对应消耗最小蓝耗 # dp2表示哪个技能实施后达到的最小蓝耗（就是去往下一个最佳状态，需要施放的技能） v = [489, 134, 186, 251, 352] w = [187, 50, 70, 95, 134] ws = [[]] * 11 W = 2988 dp = [] dp2 = [] skills = ["skill_i", "skill_j", "skill_k", "skill_l", "skill_u"] for i in range(11): for j in range(len(skills)): w[j] = w[j] + hero['Power']['Attack_increase'][i][skills[j]] ws[i] = copy.deepcopy(w) t1, t2 = knapsack01D2(ws[i], v, W) dp.append(copy.deepcopy(t1)) dp2.append(copy.deepcopy(t2)) # matrix[i][j]表示i次强化后，怪物j被打败之后，剩余的蓝量 matrix = [] for i in range(11): matrix.append([-1]*9) bosses_hp = [396, 532, 643, 668, 687, 852, 1502, 1611, 2988] bosses_award = [1695, 1678, 1362, 1458, 2659, 4157, 3089, 6493, 0] initial = 1806-134 for i in range(11): if initial >= (i+1)*233+dp[i][bosses_hp[0]]: matrix[i][0] = initial-(i+1)*233-dp[i][bosses_hp[0]] if matrix[i][0] >= 0: matrix[i][0] += bosses_award[0] for i in range(9)[1:]: if matrix[0][i-1] >= dp[0][bosses_hp[i]]: matrix[0][i] = matrix[0][i-1] - dp[0][bosses_hp[i]] if matrix[0][i] >= 0: matrix[0][i] += bosses_award[i] print(dp[6][1502]) tracing = [[]]*9 for j in range(9)[1:]: r = 0 for i in range(11)[1:]: for k in range(i+1): if matrix[k][j-1] >= 233*(i-k) and matrix[k][j-1]-233*(i-k)-dp[i][bosses_hp[j]] >= 0: matrix[i][j] = max(matrix[i][j], matrix[k][j-1]-233*(i-k)-dp[i][bosses_hp[j]]) if matrix[i][j] > r: r = matrix[i][j] tracing[j] = [k, i] if matrix[i][j] >= 0: matrix[i][j] += bosses_award[j] matrix矩阵的最终结果：[[2096, 2382, 2065, 1779, -1, -1, -1, -1, -1], [1897, 2232, 1967, 1743, 2671, 4680, 3981, -1, -1], [1696, 2067, 1851, 1666, -1, 4508, 3923, -1, -1], [-1, 1866, 1692, 1556, -1, 4331, 3844, 6507, -1], [-1, 1679, 1548, 1461, -1, 4160, 3777, 6547, -1], [-1, -1, 1364, -1, -1, -1, 3642, 6520, -1], [-1, -1, -1, -1, -1, -1, 3510, 6502, 2], [-1, -1, -1, -1, -1, -1, 3382, -1, -1], [-1, -1, -1, -1, -1, -1, 3236, -1, -1], [-1, -1, -1, -1, -1, -1, 3095, -1, -1], [-1, -1, -1, -1, -1, -1, -1, -1, -1]] 可以看到，最终要打败第9个怪物，只有一种升级的可能性，其他升级方法都不可行，但是具体怎么升级呢？眼睛看，也能看出来，就没有写代码找了，因为需要升级的话，肯定是越早越好的，应该在打第一个怪物时做1次升级（本来游戏规定要升级1次，也就是第1行已经是升级过的情况），第5个怪物升级3次，第6个怪物再升级2次，叭叭叭，顺利通关～～结论这题的关键是怎么把背包问题建模，并且有连续好几个动态规划的子问题，层层相扣，加上出题人卡的数值，刚刚好只有一个解，我在疯狂尝试我自己方法的时候，也是差不多无助的状态，等到看到提示之后，又充满信心，解决了问题，也算是波谷到波峰的爽感。类似动态规划问题，在极客赛可能算简单的，但是放在平时刷题时，算难度高的，涉及到好几个状态矩阵的计算，又是一个和一个相关联的情况。挺有意思的一题，后面再继续写一篇极客挑战赛～
最近参加了一场极客挑战赛，挺有意思的，准备简单写下笔记。整个比赛是以游戏形式让大家参与的，一共四个，一开始的时候，我以为真的可以靠纯手打，去进行通关，后来发现，想得太天真了。四个游戏，都是需要通过一些编程去解决的，我过了大部分关卡，不过有些是靠的运气，看完官方的题解后，又有了新的认知。第一个游戏是类似回合制RPG游戏，类似：画面没这么好哈，版权原因不能用游戏原图，总之有一个行动条，到了时间就可以行动，英雄（就1个）和怪物（一场战斗也就1个）互相攻击，主角在一个地下城堡，总共24层，一层一层往下冲关，每层会有一个宝箱，每次进入下一层，地图都是随机选择的，地图模式有简单、困难和BOSS，每经过8层，地图中怪兽会升级。英雄是有技能池的，选择什么技能攻击怪兽，是可以进行策略选择的。英雄的数据：hero = { "Attack": 30, "Hp": 200, "Mp": 200, "Skills": { "skill_0": { "Cost_mp": 0, "Power": 1 }, "skill_1": { "Cost_mp": 10, "Power": 1.5 }, "skill_2": { "Cost_mp": 20, "Power": 2 }, "skill_3": { "Cost_mp": 30, "Power": 2.5 } }, "Speed": 30 }典型的怪物数据："Monsters":[ { "Attack":30, "Hp":40, "Speed":25 }, { "Attack":20, "Hp":50, "Speed":20 } ]上面的Monsters数据是简单地图的，字段都还比较好理解哈，不多做解释了。游戏还有一个设定，如果是非8、16、24层，每层都有一个宝箱，这个宝箱是随机开出来奖励或者惩罚，一共有4种情况：empty_box，空箱子；add_hp_mp，加血又加魔法值，Hp+10且Mp+50；del_hp，扣10血；升级属性，可以选择Attack+10，Speed+1，Hp+100，Mp+200；是不是感觉这些写出来变量都有点多了，而且都是靠自己抓包，分析出来的，其实玩的时候，真的需要一点点琢磨。然后我通过手打1～8层，发现了一些规律：如果英雄和怪兽Speed一样，英雄是占先的，也就是速度这块，玩家是占点便宜的；通过抓包发现，整个游戏和后端交互的协议就三条吧，new_level（重开游戏），next_layer（进入下一层），和open_box（开宝箱）；new_level请求和回包：{token: "YOUR_TOKEN"} {"code":0,"data":{"hero":{"Attack":30,"Hp":200,"Mp":200,"Skills":{"skill_0":{"Cost_mp":0,"Power":1},"skill_1":{"Cost_mp":10,"Power":1.5},"skill_2":{"Cost_mp":20,"Power":2},"skill_3":{"Cost_mp":30,"Power":2.5}},"Speed":30},"star_num":3},"message":""} open_box请求和回包：{"Layer_idx":1,"token":"YOUR_TOKEN"} {"code":0,"data":{"box":{"BoxType":"add_hp_mp","Value":[30,50]}},"message":""} next_layer请求和回包：{"Layer_idx":1,"Choice":"left","Layer_ops":[["start_battle",0],["battle_op",170,200,[["Hero","skill_0"],["Monster","skill_0"],["Hero","skill_0"]]],["start_battle",1],["battle_op",150,190,[["Hero","skill_1"],["Monster","skill_0"],["Hero","skill_0"]]],["open_box"]],"token":"06xyzzou0074CFBD948B14E238FB24BC"} {"code":0,"data":{"layer":{"Layer_idx":2,"Map_mode":"boss_map","Monsters":[{"Attack":30,"Hp":100,"Speed":40}]}},"message":""} 在客户端清理了一层的怪物后，可以选择去下一层的两扇门（left or right），这时候客户端会发送后端，玩家在这一层执行的所有动作序列（看上面的示例），然后后端会根据客户端上传的序列，在后台重放，看看动作序列是否合法，合法就可以进入下一层；上面这些都是我通过手打1～8层，发现的一些规律，然后我进入第9层，发现怪物确实变强了些，那么通过上面的观察，我第一个想法是每次对怪物的击打，都要经过最优策略的选择，这个最优策略我想了下，每次打怪物的时候，我最终击败他时，保证Hp+Mp是最大的，这个算法我通过bsf，广度优先搜索，大概写了3个小时（也debug了一会），终于写出来了，其实最开始，我天真的以为每次只要固定动作序列即可，什么意思，就是我手动计算一遍简单、困难和boss的地图，会产生什么合法的对列，然后直接发几个构造好的next_layer的请求即可，后来发现，果然是天真了，极客挑战赛，不可能是这么蠢的解法（当然我觉得也是有可能能通过的，毕竟怪物属性值不是随机的，就那么几种）。但是作为程序员，怎么能都通过手动算呢，所以我就写了一个和怪物战斗时的最优策略选择算法。代码不贴了，思路是这样的：通过计算总共可能的最多攻击次数，英雄最多的攻击次数 = 怪物血量/英雄普通攻击（不耗魔的那个），怪物攻击的最多次数 = 英雄血量/怪物攻击力，选较小的次数保留下来；在最小的次数规定的时间内，把英雄和怪物可以行动的时间点插入到一个数组（理论上速度够快的一方，其实可以“套圈”，当然实际游戏数据中未出现这种数据）；把2计算得到的数组进行排序，得到一个按时间排序好的有序时间数组，数组元素记录了轮到哪一方行动，这里有一个小tip，因为上面发现了英雄和怪物速度一样的时候，英雄占先，所以偷偷给怪物行动时间加一个小的偏移量，这样排序后的结果保证了相同速度时，英雄在怪物前行动；按上述有序数组，挨个做动作，如果怪物动，就一种行动，用他的普通攻击攻击英雄，如果英雄动，英雄有4种行动可选，挨个试，合法状态继续bfs；直到怪物死掉，这个动作序列是有效的，保存下来（不仅保存动作，也保存英雄剩余的Hp和Mp）；得到所有可能的有效动作序列后（有效指的是英雄杀死了怪物），看看哪一个序列Hp+Mp最大，执行这个动作序列；宝箱我就每层都打开，这里也有策略，因为有一个宝箱是提升英雄属性，我一开始是Hp<1.5Mp，加Hp，Mp<150加Mp，Hp和Mp都健康，加Attack，后来发现这个策略不行，第二阶段（9～16层），里面有个怪物的Speed是31，英雄默认的Speed是30，这不欺负人吗？果断在1～8层有机会先Speed+1，然后尽可能在血量健康状态下加Attack，这样跑了4个小时，被我跑到了接近16层，但是后面还是会挂，那怎么办呢，嗯，我想到了，启动一个后台进程，一直跑，嘻嘻。。。一值跑的过程很煎熬，因为不知道这么做是不是能打穿24层的地堡，我当时觉得组委会肯定不是这么设计题目的，后来证明这确实是暴力过题的一种方法，跑了20+小时，我查看了日志，发现一个问题，有些运气爆棚的时候，确实可以避开扣血宝箱，而且尽可能加属性，加Hp和Mp，地图也是大多数是简单的，达到过21层，在22层时，遇到一个攻击400，Speed超级快的Boss，基本没有还手之力，我突然郁闷了。。。在内部的讨论群，我发现很多人都说自己就是暴力通过的，得靠人品，所以又等了接近一天，皇天不负有心人，真的通过啦，那一刻，我快乐得像个孩子，好开心啊～但是故事还没结束，官方题解下来后，我发现自己漏了一个（或者不是漏了，是根本没往那方面想）点，在第七层，有一段代码会展示给选手，大概是这样的：func ProcessOneUser(nowLayerId int, queryCMD string, choice string) string { r := rand.New(rand.NewSource(time.Now().UnixNano())) for { if queryCMD == "open_box" { if nowLayerId == 8 || nowLayerId ==16 || nowLayerId == 24 { return "new_star" } else if nowLayerId == 7 { return "secret_code" } else { switch r.Int63() % 4 { case 0: return "empty_box" case 1: return "add_hp_mp" case 2: return "del_hp" case 3: return "add_skill" } } } else if queryCMD == "next_layer" { newLayerId := nowLayerId + 1 // rest_map 休息房，没有怪物 // easy_map 简单层，有两个怪物 // hard_map 困难层，有三个怪物 // boss_map boss层，有一个boss if newLayerId == 8 || newLayerId == 16 || newLayerId == 24 { return "rest_map" } else { switch r.Int63() % 6 { case 0: if choice == "left" { return "easy_map" } else { return "hard_map" } case 1: if choice == "left" { return "hard_map" } else { return "easy_map" } case 2: if choice == "left" { return "easy_map" } else { return "boss_map" } case 3: if choice == "left" { return "boss_map" } else { return "easy_map" } case 4: if choice == "left" { return "hard_map" } else { return "boss_map" } case 5: if choice == "left" { return "boss_map" } else { return "hard_map" } } } } } } 当时我手打的，我也看到过这段代码，但当时我看了半天，只做了一件事情，测试了一下选择"left"和选择“right”门，简单、困难和Boss地图的几率是不是一样的，我发现次数足够大时，是一样的，当时也就放弃了，其实本题的奥义就在此。关键代码是这句：r := rand.New(rand.NewSource(time.Now().UnixNano())) 首先随机种子在每一次new_level之后只取一次，后面都是用这个随机种子生成的rng去生成随机数，如果我们能知道这个随机数，那岂不是开了天眼，每次都选择easy map即可，那怎么才能知道这个随机数呢？看一下Go的官方库rng随机数的实现：// Seed uses the provided seed value to initialize the generator to a deterministic state. func (rng *rngSource) Seed(seed int64) { rng.tap = 0 rng.feed = rngLen - rngTap seed = seed % int32max if seed < 0 { seed += int32max } if seed == 0 { seed = 89482311 } x := int32(seed) for i := -20; i < rngLen; i++ { x = seedrand(x) if i >= 0 { var u int64 u = int64(x) << 40 x = seedrand(x) u ^= int64(x) << 20 x = seedrand(x) u ^= int64(x) u ^= rngCooked[i] rng.vec[i] = u } } } 敲黑板，注意这句seed = seed % int32max，int64的seed会进行截断，而后台服务器使用的是nanoseconds时间戳，也就是后半段表示nanoseconds的时间戳（如果是前半段时间戳，就结束了），官方给出的意思是可以通过枚举所有1～math.MaxInt32的候选种子，看看他们前几个随机的数字是多少，通过开宝箱、进入下一层，玩家是可以观察到，我开的是哪种类型的宝箱，下一层地图是哪种模式，比如我玩到第10层，观察到的结果是：next Map_mode easy_map BoxType add_hp_mp next Map_mode hard_map BoxType add_hp_mp next Map_mode easy_map BoxType add_skill next Map_mode easy_map BoxType del_hp next Map_mode boss_map BoxType add_hp_mp next Map_mode boss_map BoxType add_skill next Map_mode hard_map next Map_mode easy_map BoxType del_hp next Map_mode hard_map地图类型和宝箱类型都能通过代码，推算出随机数取4或者6模是什么数值，我上面的序列对应序列是0111030221231021，这个自己想下，应该比较容易想明白吧。那枚举所有1～math.MaxInt32，也通过一样的操作，得到可能的宝箱和地图类型序列，结果存在一个map中，key是宝箱和地图类型序列，value就是seed，用自己观察到的序列查询下map，既可以找到这个seed。我试了下，如果要枚举所有的math.MaxInt32种子，还是很耗时的，当然完全通过并行计算，拆成30个并发任务，每个任务算个1亿多的种子，这也行，但是有参赛的极客想到了一个更好的策略，假设服务器的时间和客户端相差不大，在客户端记录下发送请求的nanosecond时间戳，然后加减10ms，去寻找这个时间戳，我自己试了下，前后10ms没有找到，然后我扩大范围，每扩大10ms，要多计算1000w的种子，我扩大到向后100ms时，激动人心的一刻到来了，我终于找到了：1655533609434000000... 1655533609435000000... 1655533609436000000... 1655533609437000000... 1655533609438000000... 1655533609439000000... 1655533609440000000... find key=[1655533609394757619] 其实，尽管在知道了反向查找随机种子之后，我还是花了非常久时间，大概有5～6个小时，才准确找到这个随机种子的，非常不容易，看来任何事情都是知易行难。但是尽管辛苦，我觉得很值得，我要求自己不仅要得到好的结果，也要用正确的方法得到好的结果，这样才是思考解决问题的正确方式吧。整个极客赛过程，都是自己和自己较劲，我真的想了很多方法，包括一上来通过抓包，慢慢摸索游戏的规律，后来写bfs找每次击败怪兽的最优策略，到后面靠运气通关，然后再去思考怎么样通过破解随机种子开天眼，想想是不是一步步成长的过程，一步步通过对的思维和方法解决问题的过程。哦，对了，这题充分说明，不要使用时间戳做随机种子，随机种子不要固定下来，嗯，否则是有可能让别人通过观察数据破解的，当然前提是对方猜测到了你的实现方式。
上海疫情封在家中有三个月，辅导了小孩子功课，每每小孩遇到不会做的题目，比她更着急。有时也会想起自己学生时代的样子，遇到难题，心中升起必须克服难题的决心，现在自己娃，怎么没有我当年的风采了。哈哈，扯远了，这两天网上看到一道小学二年级的题目，其实我之前做过类似的题目，但是当下还是震惊了，怎么现在小学二年级题目有这么难？题目是这样的：配合网友的回复，我马上想到了之前类似的问题，说是25匹马，5个赛道，至少比几场保证找出最快的2匹马。赛马这题答案我记得很清楚7场比赛，先分组进行小组赛：首先一个虚线框就是一场比赛，上来25匹马，分5组，进行5场预赛。5组比赛会得到组内的名次，比如A组，A1->A2->A3->A4->A5，每组第一名就是A1，B1，C1，D1和E1，然后第一名之间安排一场决赛：假设结果就是A1->B1->C1->D1->E1(真正结果不重要，不管怎么样，总能推理出最后的结论)，这时候我们就通过4场比赛，知道了第一名是A1，那第二名是谁呢，是不是B1，不一定哦，有可能是在小组赛中被A1淘汰的A2，所以安排场附加赛吧，A2 vs B1：附加赛之后，就能知道前两名了。话说回小学二年级赛车这道题目，其实思路一模一样，就是通过小组赛->决赛->附加赛，完成前两名的确认，也就是说赛车的话，通过至少5场比赛，保证找出前两名的赛车。作为小学二年级的题目很变态了吧，但是出题的人还多问了一句，怎么证明这件事情呢，为什么通过4场比赛一定不能保证找出前两名呢？这个证明难倒我了，抓耳挠腮半天，没想出来。然后我就看了下出题老师的分析，茅塞顿开，原来还可以使用图论+反正法来证明。大致的过程如下：首先把每一辆赛车想象成点，那么我们一共有9辆赛车，一共9个点，每场比赛，只能进行3辆赛车的比拼，每次比拼完，我们会有一个名次，紧挨着的前后两名之间，使用一条有向边连接，这个我文字讲得有点绕口，一图解千愁：圆圈表示赛车，1号比2号快，2号比3号快，所以每次比赛，我们仅仅能得到2条有向边，如果进行4场比赛，那么总共的有向边数量是4*2 = 8条，而赛车的数量是9，也就是节点数量是9，8条边，9个点，如果要保证所有边都连接到一张图中，不能出现环，否则边的数量不够。出现环就意味着9个点形成了若干个分开的子图，上面图中9个节点，分为了两个子图，那么就会有一个问题，第一名都不知道是谁，为什么？因为在1～8节点所在的图中，1号赛车最快，9号赛车单独成子图，9号赛车和谁都没有比过，那么到底9号和1号赛车谁快呢？不知道。所以8条边，安排的比赛结果，划分为多个子图，这样是无法确认前两名的。那我们4场比赛的结果，如何是可以前两名的图形呢，如下：左边就是分3场比赛（1、4、7，2、5、8和3、6、9分别比赛），得到结果后，4、2、3比一次，结果是4->2->3，转化这张单图，得到右侧的结果，其实就是一个树形的图，那么前两名就是1、4号赛车。这样我们很容易举出反例，你不是就8条边么，我最后4、2、3的结果是2->4->3，图就变成非树形图，如下：这样又回到之前多个子图的问题，我们是没法确认1号赛车和2号赛车谁快的，也就没法真正确认前两名是哪两辆赛车。OK，至少5场比赛，而非4场比赛保证找出最快的2辆赛车得证。是不是挺难的，至少我是没有想出来，通过图论+反证法证明这个数据结论，由衷发出感慨，活学活用好难啊，能做到的大佬，真的太强了。